Skip to content

feat(sharing): introduce friend settings schema and refine share request logic #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
May 16, 2025

Conversation

Gravender
Copy link
Owner

@Gravender Gravender commented May 11, 2025

✅ Checklist

  • The PR title follows the convention we established conventional-commit
  • I performed a functional test on my final commit

Changelog

  • Add new friendSetting and friendPlayer schemas to support per-friend sharing preferences
  • Update default values in the friendSetting schema to allow shared games, matches, players, and locations by default
  • Refine sharing logic to leverage friend settings for granular control over shared matches, locations, and players
  • Implement status handling on share requests (auto-accept vs. pending) based on friend preferences
  • Optimize share request handling by consolidating transaction management and standardizing error handling
  • Streamline share request creation to support multiple item types (games, matches, locations, players, scoresheets)
  • Prepare detailed database migrations and relationship updates to support the above changes

Summary by CodeRabbit

  • New Features

    • Added automatic sharing of newly created matches with friends based on their sharing preferences.
    • Introduced friend-specific sharing settings with granular control over matches, games, players, and locations.
    • Enhanced friend data queries to include shared items, settings, and detailed player match statistics.
    • Added new friend profile, shared items, and stats pages with rich statistics and sharing overviews.
    • Implemented friend requests management with accept, reject, and cancel actions in a tabbed interface.
    • Introduced skeleton loading components for friends list, friend requests, and friend profiles.
    • Added comprehensive friend settings dialog and form for managing sharing preferences.
    • Added new friends list component with search and enriched user info.
    • Introduced add friend dialog supporting friend requests by email or username.
    • Added reusable formatted date component for consistent date display across the UI.
  • Enhancements

    • Improved breadcrumb navigation with dynamic resolution of resource names and paths.
    • Updated player identification logic to use a dedicated isUser flag for consistency.
    • Refined friends list UI with search, avatars, and enriched user info from Clerk.
    • Enhanced sharing operations with nested transactions, permission validations, and auto-accept handling.
    • Aggregated and displayed detailed friend statistics including match outcomes and game performance.
    • Simplified and unified share request mutations with improved transactional integrity and error handling.
    • Added utility functions for mapping matches and shared players consistently.
    • Updated UI components for friend cards and friend request cards with improved data handling and visuals.
    • Replaced multiple inline date formatting usages with a reusable formatted date component for consistent display.
    • Improved key assignments in React lists for uniqueness and stability.
  • Bug Fixes

    • Fixed player and shared player user identification by replacing user ID comparisons with isUser flags.
    • Corrected import paths and fallback handling in friend profile and shared items components.
    • Fixed React key assignments in various lists to ensure uniqueness.
  • Database & Backend

    • Added friend_setting table to store friend-specific sharing preferences.
    • Modified player schema to replace userId with isUser boolean and optional friendId.
    • Updated relational schema to integrate friend and friend settings relations.
    • Improved transactional integrity and error handling in share request mutations.
    • Added explicit typings for database client and transactions.
    • Seed data updated to include friend settings with randomized sharing configurations.
  • Chores

    • Updated dependencies and devDependencies versions across multiple packages.
    • Refactored ESLint and Prettier configurations and removed deprecated plugins.
    • Removed legacy friend-tab component in favor of modular friends list and requests tabs.
    • Adjusted Next.js image remote patterns and minor UI comment cleanup.

Gravender added 9 commits May 12, 2025 01:21
- Introduce new friendPlayer and friendSetting tables to manage relationships between users and their friends, including permissions and settings.
- Update relations in index.ts to include new relationships for friendPlayers and friendSettings, enhancing data structure and retrieval capabilities.
- Change default values for allowSharedGames, allowSharedPlayers, allowSharedLocation, and allowSharedMatches to true in the friendSetting schema, enhancing user permissions and settings management.
…saction management

- Refactor share request logic to utilize database transactions for better consistency and error handling.
- Update share request creation process to handle multiple item types (games, matches, locations, players, scoresheet) more efficiently.
- Enhance error handling and response messages for improved user feedback during sharing operations.
… settings

- Implement status assignment for share requests, setting it to "accepted" or "pending" based on user friend settings for players and games.
- Enhance share request creation logic to improve user experience and streamline sharing operations.
…friend validation

- Introduce handling for shared locations in share requests, ensuring proper validation of friends before sharing.
- Update transaction management to improve error handling and streamline the sharing process for games, matches, and locations.
- Refactor existing logic to incorporate new shared location schema and enhance user feedback during sharing operations.
- Replace instances of the transaction variable to ensure consistent usage of `tx2` for database queries and inserts in share request handling.
- Improve code clarity and maintainability by standardizing transaction management across various share request operations.
…anagement

- Consolidate transaction handling in share request logic to improve consistency and maintainability.
- Streamline the process for sharing players, matches, and locations, ensuring proper validation and error handling.
- Enhance user feedback by refining response messages during sharing operations.
…ations, and players

- Enhance match creation process to include sharing functionality with friends based on their settings.
- Introduce detailed transaction management for sharing matches, locations, and players, ensuring proper validation and error handling.
- Streamline the sharing process by incorporating new schemas for shared matches, locations, and players, improving user experience and feedback during sharing operations.
…n handling

- Update sharing functionality to utilize friend settings more effectively, allowing for granular control over shared matches, locations, and players.
- Introduce new relationships in the database schema to support friend connections and their associated settings.
- Improve transaction management for share requests, ensuring accurate permission assignment and status handling based on friend configurations.
@Gravender Gravender self-assigned this May 11, 2025
Copy link

vercel bot commented May 11, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
games ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 16, 2025 9:42am

Copy link

coderabbitai bot commented May 11, 2025

Walkthrough

This update introduces a comprehensive friend management and sharing system across the application. It adds friend-specific sharing settings, enables granular control over shared matches, games, players, and locations, and integrates Clerk user data for enriched friend profiles. The database schema and API logic are refactored to use a boolean isUser flag for identifying user-linked players, replacing direct user ID associations. The Next.js dashboard receives new components and pages for friend profiles, settings, shared items, and statistics, with improved skeleton loaders and tabbed interfaces for friend requests and lists.

Changes

Files / Paths Change Summary
packages/db/src/schema/player.ts, packages/db/src/relations/index.ts, packages/db/src/seeding/seed.ts, packages/api/src/routers/players.ts, packages/api/src/routers/game.ts, packages/api/src/routers/location.ts, packages/api/src/routers/sharing/share-game.ts, packages/api/src/routers/sharing/share-location.ts, packages/api/src/routers/match.ts, packages/api/src/routers/dashboard.ts, packages/api/src/trpc.ts, packages/api/src/routers/user.ts Replaced userId field on player with isUser boolean flag; updated all logic, queries, and relations to use isUser for identifying user-linked players.
packages/db/src/schema/friendSetting.ts, packages/db/src/schema/index.ts, packages/db/src/relations/index.ts Added friend_setting table schema, exported it, and integrated it into relations for user, friend, and player entities.
packages/api/src/routers/friends.ts, packages/api/src/utils/sharing.ts, packages/api/src/utils/game.ts Implemented friend profile enrichment with Clerk user data, aggregation of shared entities, new friend settings mutation, and utility functions for mapping matches and shared items.
packages/api/src/routers/match.ts, packages/api/src/routers/sharing/share-request.ts Overhauled match creation and sharing logic to support automatic and permission-based sharing with friends, modularized sharing helpers, and ensured transactional integrity.
packages/api/src/routers/dashboard.ts, apps/nextjs/src/components/breadcrumbs.tsx Rewrote breadcrumb generation to support dynamic, segment-based navigation with enriched resource names and shared entity support.
apps/nextjs/src/app/dashboard/friends/_components/friend-list.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-list-skeleton.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-requests-tabs.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-requests-skeleton.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-card.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx Introduced new friends list, skeletons, requests tabs, and refactored cards to use unified friend/request objects with avatars, search, and improved UI/UX.
apps/nextjs/src/app/dashboard/friends/_components/friend-tab.tsx Removed legacy friends tab component.
apps/nextjs/src/app/dashboard/friends/page.tsx Updated page layout to use new friends list and requests tabs with suspense and skeletons.
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx, apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-skeleton.tsx, apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-form.tsx, apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-dialog.tsx, apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items-page.tsx, apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items.tsx, apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx Added new components for friend profile, settings, shared items, and stats pages, with client-side state, forms, and search/filtering.
apps/nextjs/src/app/dashboard/friends/[id]/page.tsx, apps/nextjs/src/app/dashboard/friends/[id]/shared/page.tsx, apps/nextjs/src/app/dashboard/friends/[id]/stats/page.tsx Added/updated routing and server components for friend profile, shared items, and stats, with suspense and skeletons.
packages/api/src/routers/sharing/share-request.ts Refactored share request logic for games, matches, players, and locations to use transactions, modular helpers, and improved permission checks.
packages/api/src/routers/sharing/share-game.ts, packages/api/src/routers/sharing/share-location.ts Updated isUser logic for shared match players.
apps/nextjs/src/app/dashboard/groups/[id]/edit/players/_components/selectPlayerForm.tsx Temporarily disabled filtering of players by group membership (marked with a TODO).
apps/nextjs/src/app/dashboard/games/page.tsx Made the Page component async and updated searchParams handling.
apps/nextjs/src/app/dashboard/players/_components/players.tsx Updated React key for player cards to include both ID and type.
apps/nextjs/src/app/dashboard/games/[id]/stats/_components/game-stats.tsx Removed optional chaining on placement property access.
packages/db/src/client.ts Added and exported explicit types for database and transaction contexts.
packages/api/package.json Updated export for ./uploadthing to include types.
apps/nextjs/package.json, package.json, packages/db/package.json, pnpm-workspace.yaml, tooling/eslint/package.json, tooling/prettier/package.json, tooling/tailwind/package.json Updated or removed various dependencies and devDependencies for maintenance.
tooling/eslint/react.js, tooling/eslint/types.d.ts Removed react-compiler plugin and related type declarations; updated react-hooks plugin import.
packages/api/src/root.ts Removed JSDoc comment for createCaller export.
packages/db/src/schema/index.ts Added export for friendSetting schema.
.vscode/settings.json Prefixed ESLint working directory patterns with ./.
packages/ui/src/sidebar.tsx Removed an internal comment only.
apps/nextjs/src/app/dashboard/games/[id]/share/_components/share-game.tsx, apps/nextjs/src/app/dashboard/games/[id]/[matchId]/share/_components/share-match.tsx, apps/nextjs/src/app/dashboard/players/[id]/share/_components/share-player.tsx Replaced friend object property access from nested friend.friendId, friend.friend.name, friend.friend.email to direct friend.id, friend.name, friend.email; replaced inline date formatting with reusable FormattedDate component.
apps/nextjs/src/components/formatted-date.tsx Added new reusable FormattedDate React component for consistent date formatting with optional icons and styling.
apps/nextjs/src/app/dashboard/friends/_components/add-friend-dialog.tsx Added new dialog component for sending friend requests by email or username with validation, mutation, and toast notifications.
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-form.tsx, apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-dialog.tsx Added friend settings form and dialog components with tabs for matches, games, players, and locations sharing preferences and permissions.
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx Added friend profile page component showing avatar, name, username, and player stats including H-index calculation.
apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items.tsx, apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items-page.tsx Added components to display shared items between user and friend with tabs, search, grouping, and detailed views.
apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx, apps/nextjs/src/app/dashboard/friends/[id]/stats/page.tsx, apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/game-details.tsx Added friend statistics page and components including summary, last play, previous matches, and sortable game details table.
apps/nextjs/src/app/dashboard/friends/page.tsx Updated friends dashboard page to new layout with two columns, suspense, and skeletons for friends list and requests tabs.
apps/nextjs/src/app/dashboard/friends/_components/friend-list.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-list-skeleton.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-requests-tabs.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-requests-skeleton.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-card.tsx, apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx Added new friends list and requests UI components with avatars, search, tabbed interface, and cancel friend request functionality.
apps/nextjs/src/app/dashboard/friends/_components/friend-tab.tsx Removed legacy friends tab component.
apps/nextjs/src/app/dashboard/friends/[id]/page.tsx Fixed import path and added Open Graph image metadata for friend profile page.
apps/nextjs/src/app/dashboard/friends/_components/friend-profile-skeleton.tsx Added skeleton UI for friend profile page.
apps/nextjs/src/app/dashboard/friends/[id]/shared/page.tsx Added server component for friend shared items page with validation and suspense.
apps/nextjs/src/app/dashboard/friends/[id]/stats/page.tsx Added server component for friend stats page with validation and suspense.
apps/nextjs/src/components/breadcrumbs.tsx, packages/api/src/routers/dashboard.ts Simplified breadcrumb component and rewrote backend breadcrumb procedure for dynamic segment-based resolution including shared entities and friends.
apps/nextjs/src/app/dashboard/games/_components/matches-list.tsx, apps/nextjs/src/app/dashboard/games/shared/[id]/[matchId]/summary/_components/shared-match-summary.tsx, apps/nextjs/src/app/dashboard/games/[id]/stats/_components/game-stats.tsx, apps/nextjs/src/app/dashboard/games/shared/[id]/stats/_components/shared-game-stats.tsx, apps/nextjs/src/app/dashboard/locations/[id]/_components/Matches.tsx, apps/nextjs/src/app/dashboard/locations/shared/[id]/_components/Matches.tsx, apps/nextjs/src/app/dashboard/players/[id]/stats/page.tsx, apps/nextjs/src/app/dashboard/share-requests/[id]/_componenets/match-request.tsx, apps/nextjs/src/app/dashboard/share-requests/[id]/_componenets/player-request.tsx Replaced inline date formatting with FormattedDate component for consistent date display with optional icons.
apps/nextjs/src/app/dashboard/games/[id]/[matchId]/summary/_components/match-summary.tsx, apps/nextjs/src/app/dashboard/players/[id]/stats/_components/GameDetailsTable.tsx, apps/nextjs/src/app/dashboard/players/_components/players.tsx Updated React keys for lists to use composite keys including type for uniqueness.
packages/api/src/utils/clerk.ts Added utility function getFullName to derive full name from Clerk user data with fallback logic.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant API
    participant DB
    participant Clerk
    participant Friend

    User->>API: Create Match
    API->>DB: Insert match, players, rounds
    API->>DB: Fetch match details & friends
    loop For each eligible Friend
        API->>DB: Check friend sharing settings
        alt Auto-sharing enabled
            API->>DB: Create share request (auto-accept if allowed)
            API->>DB: Share match, location, game, players as needed
        else
            API->>DB: Create share request (pending)
        end
    end
    API->>User: Return created match (shared as needed)
Loading
sequenceDiagram
    participant User
    participant API
    participant DB
    participant Clerk

    User->>API: View Friend Profile
    API->>DB: Fetch friend, linked player, shared entities, settings
    API->>Clerk: Fetch Clerk user data for friend
    API->>User: Return profile, stats, shared items, settings
Loading

Poem

🐰✨
With friends now closer, sharing’s a breeze,
Matches and games sent with ease.
Settings to tweak, permissions to grant,
Avatars, stats—oh, what a slant!
Breadcrumbs are smarter, profiles are bright,
Rabbits rejoice—friendship’s delight!
🥕

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 🔥 Error while generating docstrings. (🔄 Check again to generate docstrings again)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🔭 Outside diff range comments (1)
packages/api/src/routers/sharing/share-request.ts (1)

665-673: 💡 Verification agent

🧩 Analysis chain

Writes executed outside the enclosing transaction break atomicity

Inside the outer ctx.db.transaction, you are calling ctx.db.insert(...) instead of tx.insert(...).
If this insert fails or any later statement in the outer transaction is rolled-back, the location share created here will remain committed, leaving the DB in an inconsistent state.

-    await ctx.db.insert(shareRequest).values({
+    await tx.insert(shareRequest).values({

Search for any other remaining ctx.db.* calls in this file to ensure every query inside the transaction uses the same tx handle.


🏁 Script executed:

#!/bin/bash
# Verify there are no stray ctx.db usages in share-request router
rg --line-number --context 2 $'ctx\\.db\\.' packages/api/src/routers/sharing/share-request.ts

Length of output: 1370


Ensure atomicity by using tx.insert inside the transaction

Inside the mutation starting at line 628 of packages/api/src/routers/sharing/share-request.ts, the insert on line 666 uses ctx.db.insert, which executes outside the enclosing tx and breaks the transaction’s atomicity. Update it as follows:

• File: packages/api/src/routers/sharing/share-request.ts
Location: within the callback of ctx.db.transaction(async (tx) => { … }), around line 666

-    await ctx.db.insert(shareRequest).values({
+    await tx.insert(shareRequest).values({
🧹 Nitpick comments (2)
packages/api/src/routers/match.ts (1)

415-619: Consider extracting the friend sharing logic to a separate function for maintainability.

While the implementation is solid, the friend sharing logic (lines 415-619) is quite complex and lengthy. Consider extracting it to a separate helper function to improve readability and maintainability of the createMatch mutation.

+ // Add this helper function before the matchRouter definition
+ async function shareMatchWithFriends(
+   tx: any, 
+   ctx: any, 
+   createdMatch: any, 
+   returningMatch: any, 
+   shareFriends: any[]
+ ) {
+   for (const shareFriend of shareFriends) {
+     await tx.transaction(async (innerTx) => {
+       // Move the entire sharing logic here
+       // Replace tx with innerTx
+       // ...existing code from lines 416-618...
+     });
+   }
+ }

// Then in the createMatch mutation, replace lines 415-619 with:
+ await shareMatchWithFriends(
+   transaction, 
+   ctx, 
+   createdMatch, 
+   returningMatch, 
+   shareFriends
+ );
packages/api/src/routers/sharing/share-request.ts (1)

68-104: Large, deeply-nested loops duplicate sharing logic – consider extracting helpers

The for … of loops for matches, locations, players and scoresheets are almost identical in the Game, Match and Player mutations. Moving this logic into reusable helper functions (e.g. createLocationShare(tx, …) or autoAcceptIfNeeded(tx, settings, …)) will:
• reduce ~1 000 lines of repetition,
• make the transaction flow easier to audit,
• improve testability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c76b796 and 001e052.

📒 Files selected for processing (6)
  • packages/api/src/routers/match.ts (3 hunks)
  • packages/api/src/routers/sharing/share-request.ts (7 hunks)
  • packages/db/src/relations/index.ts (4 hunks)
  • packages/db/src/schema/friendPlayer.ts (1 hunks)
  • packages/db/src/schema/friendSetting.ts (1 hunks)
  • packages/db/src/schema/index.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/api/src/routers/match.ts (1)
packages/db/src/zodSchema/index.ts (2)
  • selectSharedLocationSchema (105-105)
  • selectSharedPlayerSchema (75-75)
packages/api/src/routers/sharing/share-request.ts (1)
packages/db/src/zodSchema/index.ts (2)
  • selectSharedLocationSchema (105-105)
  • selectSharedPlayerSchema (75-75)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint
🔇 Additional comments (12)
packages/db/src/schema/index.ts (1)

24-25: Well-structured schema exports for the new friend sharing system.

These export declarations appropriately expose the new friendPlayer and friendSetting schemas, which form the foundation of the per-friend sharing preferences system described in the PR objectives.

packages/db/src/schema/friendPlayer.ts (2)

17-50: Great schema design with appropriate constraints and permissions model.

The friend_player table schema effectively links friends to players with permission controls. The use of a permission enum with "view"/"edit" options provides the granular control described in the PR objectives, while the default of "view" implements principle of least privilege.


40-48: Thorough index implementation for query optimization.

The indexes on createdById, friendId, and playerId will improve query performance for the relationship-heavy queries seen in the match router. The unique constraint on friendId and playerId correctly prevents duplicate relationships.

packages/db/src/schema/friendSetting.ts (2)

16-63: Comprehensive friend settings schema with secure defaults.

The schema implements an extensive set of permission and sharing controls while maintaining secure defaults. Auto-sharing features default to false requiring explicit opt-in, while the ability to receive shares defaults to true for good user experience.


70-78: Well-implemented indexes and constraints.

The indexes will optimize the queries seen in the match router that filter based on these columns. The unique constraint correctly ensures only one settings record exists per creator-friend relationship.

packages/api/src/routers/match.ts (7)

327-374: Thorough match query with deep relational data.

The query retrieves all necessary data for sharing decisions in a single operation, including nested friend relationships and settings. This approach is efficient and avoids multiple queries.


375-414: Effective friend filtering based on sharing permissions.

The code elegantly filters friends eligible for sharing by evaluating both the friend's settings (autoShareMatches) and the user's own settings (allowSharedMatches), ensuring mutual consent for sharing.


415-437: Well-implemented transactional share request creation.

Using nested transactions ensures atomicity when creating share requests. The status is correctly set based on autoAcceptMatches preference, automatically differentiating between auto-accepted and pending shares.


438-484: Thorough location sharing with existence checks.

The location sharing logic properly checks for existing shared locations before creating new ones, preventing duplicates. The permission settings are correctly propagated from friend settings.


485-526: Efficient game sharing with reuse of existing shares.

The code first checks for existing shared games before creating new ones, preventing duplicate shares and efficiently reusing existing relationships.


527-549: Conditional shared match creation with proper references.

The shared match creation correctly references the shared game and location (if available), maintaining referential integrity in the sharing graph.


550-617: Comprehensive player sharing with permission inheritance.

The player sharing implementation handles both direct player sharing and match player association, correctly linking shared players to shared matches when appropriate. The permission model is consistently applied across all entity types.

Gravender added 2 commits May 12, 2025 04:27
…riendPlayer schema

- Replace transaction variable in share request logic for consistency.
- Modify friendSettings relation to support multiple friend settings.
- Enhance unique constraints in friendPlayer schema to improve data integrity and relationships.
…improved maintainability

- Refactor sharing functionality into a new `shareMatchWithFriends` function to streamline the process of sharing matches, locations, and players with friends.
- Enhance transaction management within the new function, ensuring consistent handling of share requests and permissions based on friend settings.
- Improve code clarity and maintainability by reducing complexity in the main match router logic.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
packages/api/src/routers/match.ts (4)

328-418: Strong match data fetching for sharing, but consider query optimization.

The code effectively fetches the newly created match with all necessary related data for sharing, but the deeply nested query might cause performance issues with large datasets.

Consider optimizing this query by:

  1. Fetching only essential data needed for sharing decisions
  2. Using separate, targeted queries for specific relationships if needed
  3. Adding pagination if dealing with potentially large numbers of players or friends
- const createdMatch = await transaction.query.match.findFirst({
-   where: {
-     id: returningMatch.id,
-   },
-   with: {
-     scoresheet: true,
-     game: true,
-     matchPlayers: {
-       with: {
-         player: {
-           with: {
-             friendPlayer: {
-               where: {
-                 createdById: ctx.userId,
-               },
-               with: {
-                 friend: {
-                   with: {
-                     friend: {
-                       with: {
-                         friends: {
-                           where: {
-                             friendId: ctx.userId,
-                           },
-                           with: {
-                             friendSetting: true,
-                           },
-                         },
-                       },
-                     },
-                     friendSetting: true,
-                   },
-                 },
-               },
-             },
-           },
-         },
-       },
-     },
-     location: true,
-   },
- });
+ // First, fetch just the match with basic relationships
+ const createdMatch = await transaction.query.match.findFirst({
+   where: { id: returningMatch.id },
+   with: {
+     scoresheet: true,
+     game: true,
+     location: true,
+     matchPlayers: {
+       with: { player: { columns: { id: true } } }
+     }
+   }
+ });
+ 
+ // Then, fetch friend settings in a separate query if needed
+ const playerIds = createdMatch.matchPlayers.map(mp => mp.player.id);
+ const friendSettings = await transaction.query.friendPlayer.findMany({
+   where: {
+     playerId: { in: playerIds },
+     createdById: ctx.userId
+   },
+   with: {
+     friend: {
+       with: {
+         friendSetting: true,
+         friend: {
+           with: {
+             friends: {
+               where: { friendId: ctx.userId },
+               with: { friendSetting: true }
+             }
+           }
+         }
+       }
+     }
+   }
+ });
+
+ // Then map friend settings to players as needed

376-416: Clean but complex filtering logic for shareable friends.

The logic to determine which friends should receive shared content is functional but complex, making maintenance challenging. Consider refactoring this block into a dedicated helper function.

- const shareFriends = createdMatch.matchPlayers
-   .flatMap((matchPlayer) => {
-     const returnedFriend = matchPlayer.player.friendPlayer?.friend;
-     const returnedFriendSetting = returnedFriend?.friend.friends.find(
-       (friend) => friend.friendId === ctx.userId,
-     )?.friendSetting;
-     if (
-       returnedFriend?.friendSetting?.autoShareMatches === true &&
-       returnedFriendSetting?.allowSharedMatches === true
-     ) {
-       return {
-         friendUserId: returnedFriend.friendId,
-         shareLocation:
-           returnedFriend.friendSetting.includeLocationWithMatch ===
-           true,
-         sharePlayers:
-           returnedFriend.friendSetting.sharePlayersWithMatch === true,
-         defaultPermissionForMatches:
-           returnedFriend.friendSetting.defaultPermissionForMatches,
-         defaultPermissionForPlayers:
-           returnedFriend.friendSetting.defaultPermissionForPlayers,
-         defaultPermissionForLocation:
-           returnedFriend.friendSetting.defaultPermissionForLocation,
-         defaultPermissionForGame:
-           returnedFriend.friendSetting.defaultPermissionForGame,
-         allowSharedPlayers:
-           returnedFriendSetting.allowSharedPlayers === true,
-         allowSharedLocation:
-           returnedFriendSetting.allowSharedLocation === true,
-         autoAcceptMatches:
-           returnedFriendSetting.autoAcceptMatches === true,
-         autoAcceptPlayers:
-           returnedFriendSetting.autoAcceptPlayers === true,
-         autoAcceptLocation:
-           returnedFriendSetting.autoAcceptLocation === true,
-       };
-     }
-     return false;
-   })
-   .filter((friend) => friend !== false);
+ // Extract to a descriptive helper function
+ const shareFriends = getShareableFriendsFromMatch(createdMatch, ctx.userId);

With a helper function definition:

function getShareableFriendsFromMatch(match, userId) {
  return match.matchPlayers
    .flatMap((matchPlayer) => {
      const returnedFriend = matchPlayer.player.friendPlayer?.friend;
      const returnedFriendSetting = returnedFriend?.friend.friends.find(
        (friend) => friend.friendId === userId,
      )?.friendSetting;
      
      // Early exit if no auto-sharing or matches not allowed
      if (
        !returnedFriend?.friendSetting?.autoShareMatches || 
        !returnedFriendSetting?.allowSharedMatches
      ) {
        return false;
      }
      
      // Return friend with all required settings
      return {
        friendUserId: returnedFriend.friendId,
        shareLocation: Boolean(returnedFriend.friendSetting.includeLocationWithMatch),
        sharePlayers: Boolean(returnedFriend.friendSetting.sharePlayersWithMatch),
        defaultPermissionForMatches: returnedFriend.friendSetting.defaultPermissionForMatches,
        defaultPermissionForPlayers: returnedFriend.friendSetting.defaultPermissionForPlayers,
        defaultPermissionForLocation: returnedFriend.friendSetting.defaultPermissionForLocation,
        defaultPermissionForGame: returnedFriend.friendSetting.defaultPermissionForGame,
        allowSharedPlayers: Boolean(returnedFriendSetting.allowSharedPlayers),
        allowSharedLocation: Boolean(returnedFriendSetting.allowSharedLocation),
        autoAcceptMatches: Boolean(returnedFriendSetting.autoAcceptMatches),
        autoAcceptPlayers: Boolean(returnedFriendSetting.autoAcceptPlayers),
        autoAcceptLocation: Boolean(returnedFriendSetting.autoAcceptLocation),
      };
    })
    .filter((friend) => friend !== false);
}

1507-1510: Complex type definition could be simplified.

The TransactionType definition uses nested type parameter extraction, which is difficult to read and understand at a glance.

- export type TransactionType = Parameters<
-   Parameters<DatabaseType["transaction"]>[0]
- >[0];
+ // More readable approach using intermediary types
+ type TransactionCallback = Parameters<DatabaseType["transaction"]>[0];
+ export type TransactionType = Parameters<TransactionCallback>[0];

1614-1654: Reused logic for finding/creating shared game could be extracted.

This pattern of checking for an existing entity and creating it if not found is repeated multiple times throughout the code. Consider extracting it to a reusable helper function.

- const existingSharedGame = await tx.query.sharedGame.findFirst({
-   where: {
-     gameId: createdMatch.game.id,
-     sharedWithId: shareFriend.friendUserId,
-     ownerId: userId,
-   },
- });
- if (!existingSharedGame) {
-   const [createdSharedGame] = await tx
-     .insert(sharedGame)
-     .values({
-       ownerId: userId,
-       sharedWithId: shareFriend.friendUserId,
-       gameId: createdMatch.game.id,
-       permission: shareFriend.defaultPermissionForGame,
-     })
-     .returning();
-   if (!createdSharedGame) {
-     throw new TRPCError({
-       code: "INTERNAL_SERVER_ERROR",
-       message: "Failed to generate share.",
-     });
-   }
-   returnedSharedGame = createdSharedGame;
- } else {
-   returnedSharedGame = existingSharedGame;
- }
+ // Using a generic helper function
+ returnedSharedGame = await findOrCreateSharedEntity(
+   tx,
+   sharedGame,
+   {
+     gameId: createdMatch.game.id,
+     sharedWithId: shareFriend.friendUserId,
+     ownerId: userId,
+   },
+   {
+     ownerId: userId,
+     sharedWithId: shareFriend.friendUserId,
+     gameId: createdMatch.game.id,
+     permission: shareFriend.defaultPermissionForGame,
+   },
+   "Failed to generate share."
+ );

With a helper function like:

async function findOrCreateSharedEntity<T extends Record<string, any>>(
  tx: TransactionType,
  table: any,
  whereClause: Record<string, any>,
  insertValues: Record<string, any>,
  errorMessage: string
): Promise<T> {
  const existing = await tx.query[table.name].findFirst({
    where: whereClause,
  });
  
  if (existing) return existing;
  
  const [created] = await tx
    .insert(table)
    .values(insertValues)
    .returning();
    
  if (!created) {
    throw new TRPCError({
      code: "INTERNAL_SERVER_ERROR",
      message: errorMessage,
    });
  }
  
  return created;
}
packages/api/src/routers/sharing/share-request.ts (3)

192-211: Good user preference validation but duplicated across mutations.

The user preference validation logic is correctly implemented but is duplicated across the three sharing mutations (requestShareGame, requestShareMatch, requestSharePlayer). This could be extracted to a reusable function.

- const recipientSettings =
-   await tx2.query.userSharingPreference.findFirst({
-     where: {
-       userId: friendToShareTo.id,
-     },
-   });
-
- if (recipientSettings?.allowSharing === "none") {
-   shareMessages.push({
-     success: false,
-     message: `User ${recipientSettings.userId} does not allow sharing.`,
-   });
-   return false;
- }
- const returnedFriend = await tx2.query.friend.findFirst({
-   where: {
-     friendId: ctx.userId,
-     userId: friendToShareTo.id,
-   },
- });
- if (!returnedFriend) {
-   shareMessages.push({
-     success: false,
-     message: `User ${friendToShareTo.id} does not exist.`,
-   });
-   return false;
- }
+ // Extract to a helper function
+ const validationResult = await validateFriendSharingPermissions(
+   tx2, 
+   ctx.userId, 
+   friendToShareTo.id
+ );
+ 
+ if (!validationResult.success) {
+   shareMessages.push({
+     success: false,
+     message: validationResult.message,
+   });
+   return false;
+ }
+ 
+ const returnedFriend = validationResult.friend;

With a helper function like:

async function validateFriendSharingPermissions(
  tx: TransactionType,
  currentUserId: number,
  friendId: number
) {
  // Check sharing preferences
  const recipientSettings = await tx.query.userSharingPreference.findFirst({
    where: { userId: friendId },
  });

  if (recipientSettings?.allowSharing === "none") {
    return {
      success: false,
      message: `User ${recipientSettings.userId} does not allow sharing.`,
    };
  }

  // Check friend relationship exists
  const returnedFriend = await tx.query.friend.findFirst({
    where: {
      friendId: currentUserId,
      userId: friendId,
    },
  });

  if (!returnedFriend) {
    return {
      success: false,
      message: `User ${friendId} does not exist.`,
    };
  }

  return {
    success: true,
    friend: returnedFriend,
  };
}

383-407: Well-implemented location sharing logic but repeats a pattern.

The location sharing logic correctly handles permission checks, share request creation, and entity creation for auto-acceptance. However, this pattern is repeated in multiple places in the code.

This functionality could be extracted to a reusable function to reduce duplication:

/**
 * Handles the sharing of a location with a friend
 * @returns The created shared location if auto-accepted, or null
 */
async function handleLocationSharing(
  tx: TransactionType,
  ownerId: number,
  locationId: number,
  friendId: number,
  parentShareId: number,
  friendSettings: any,
  permission: string,
  expiresAt: Date | null
) {
  // Check for existing share request
  const existingShareLocation = await tx.query.shareRequest.findFirst({
    where: {
      OR: [
        {
          itemId: locationId,
          itemType: "location",
          ownerId: ownerId,
          sharedWithId: friendId,
          parentShareId: parentShareId,
        },
        {
          itemId: locationId,
          itemType: "location",
          ownerId: ownerId,
          sharedWithId: friendId,
          status: "accepted",
        },
      ],
    },
  });
  
  // Skip if already shared
  if (!existingShareLocation) {
    // Create share request
    await tx.insert(shareRequest).values({
      ownerId: ownerId,
      sharedWithId: friendId,
      itemType: "location",
      itemId: locationId,
      permission: permission,
      status: friendSettings?.autoAcceptLocation ? "accepted" : "pending",
      parentShareId: parentShareId,
      expiresAt: expiresAt,
    });
    
    // Handle auto-acceptance if enabled
    if (friendSettings?.autoAcceptLocation) {
      const existingSharedLocation = await tx.query.sharedLocation.findFirst({
        where: {
          locationId: locationId,
          sharedWithId: friendId,
          ownerId: ownerId,
        },
      });
      
      if (!existingSharedLocation) {
        const [createdSharedLocation] = await tx
          .insert(sharedLocation)
          .values({
            ownerId: ownerId,
            sharedWithId: friendId,
            locationId: locationId,
            permission: friendSettings.defaultPermissionForLocation,
          })
          .returning();
          
        if (!createdSharedLocation) {
          throw new TRPCError({
            code: "INTERNAL_SERVER_ERROR",
            message: "Failed to generate location share.",
          });
        }
        
        return createdSharedLocation;
      } else {
        return existingSharedLocation;
      }
    }
  }
  
  return null;
}

This function could then be used in all places where location sharing needs to be handled.


1783-1957: Block creates deeply nested conditionals that reduce readability.

The player sharing logic within match sharing contains deeply nested conditionals that reduce code readability and make maintenance difficult.

Consider refactoring with early returns or by extracting logic into separate functions:

- if (!createdSharedScoresheet) {
-   throw new TRPCError({
-     code: "INTERNAL_SERVER_ERROR",
-     message: "Failed to generate share.",
-   });
- }
- }
- }
- }
- let returnedSharedMatch: z.infer<
-   typeof selectSharedMatchSchema
- > | null = null;
- if (friendSettings?.autoAcceptMatches) {
-   const existingSharedMatch =
-     await tx2.query.sharedMatch.findFirst({
-       where: {
-         matchId: matchToShare.matchId,
-         sharedWithId: friendToShareTo.id,
-         ownerId: ctx.userId,
-       },
-     });
-   if (!existingSharedMatch) {
-     if (!returnedSharedGame) {
-       const existingSharedGame =
-         await tx2.query.sharedGame.findFirst({
-           where: {
-             gameId: returnedMatch.gameId,
-             sharedWithId: friendToShareTo.id,
-             ownerId: ctx.userId,
-           },
-         });
-       if (!existingSharedGame) {
-         throw new TRPCError({
-           code: "INTERNAL_SERVER_ERROR",
-           message: "Failed to generate share.",
-         });
-       }
-       returnedSharedGame = existingSharedGame;
-     }
-     const [createdSharedMatch] = await tx2
-       .insert(sharedMatch)
-       .values({
-         ownerId: ctx.userId,
-         sharedWithId: friendToShareTo.id,
-         sharedGameId: returnedSharedGame.id,
-         matchId: matchToShare.matchId,
-         sharedLocationId:
-           returnedSharedLocation?.id ?? undefined,
-         permission: friendSettings.defaultPermissionForMatches,
-       })
-       .returning();
-     if (!createdSharedMatch) {
-       throw new TRPCError({
-         code: "INTERNAL_SERVER_ERROR",
-         message: "Failed to generate share.",
-       });
-     }
-     returnedSharedMatch = createdSharedMatch;
-   } else {
-     returnedSharedMatch = existingSharedMatch;
-   }
- }
+ // Extract to a dedicated function that handles all the match sharing logic
+ let returnedSharedMatch = await handleMatchSharing(
+   tx2,
+   ctx.userId,
+   friendToShareTo.id,
+   matchToShare.matchId,
+   returnedMatch.gameId,
+   returnedSharedGame,
+   returnedSharedLocation,
+   friendSettings
+ );

With helper functions like:

async function handleMatchSharing(
  tx: TransactionType,
  ownerId: number,
  friendId: number,
  matchId: number,
  gameId: number,
  sharedGame: any | null,
  sharedLocation: any | null,
  friendSettings: any
) {
  // Skip if auto-accept not enabled
  if (!friendSettings?.autoAcceptMatches) {
    return null;
  }
  
  // Check for existing shared match
  const existingSharedMatch = await tx.query.sharedMatch.findFirst({
    where: {
      matchId: matchId,
      sharedWithId: friendId,
      ownerId: ownerId,
    },
  });
  
  if (existingSharedMatch) {
    return existingSharedMatch;
  }
  
  // Ensure we have a shared game reference
  let sharedGameRef = sharedGame;
  if (!sharedGameRef) {
    sharedGameRef = await tx.query.sharedGame.findFirst({
      where: {
        gameId: gameId,
        sharedWithId: friendId,
        ownerId: ownerId,
      },
    });
    
    if (!sharedGameRef) {
      throw new TRPCError({
        code: "INTERNAL_SERVER_ERROR",
        message: "Failed to generate share: shared game not found.",
      });
    }
  }
  
  // Create the shared match
  const [createdSharedMatch] = await tx
    .insert(sharedMatch)
    .values({
      ownerId: ownerId,
      sharedWithId: friendId,
      sharedGameId: sharedGameRef.id,
      matchId: matchId,
      sharedLocationId: sharedLocation?.id,
      permission: friendSettings.defaultPermissionForMatches,
    })
    .returning();
    
  if (!createdSharedMatch) {
    throw new TRPCError({
      code: "INTERNAL_SERVER_ERROR",
      message: "Failed to generate share.",
    });
  }
  
  return createdSharedMatch;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 001e052 and 024d012.

📒 Files selected for processing (4)
  • packages/api/src/routers/match.ts (4 hunks)
  • packages/api/src/routers/sharing/share-request.ts (8 hunks)
  • packages/db/src/relations/index.ts (4 hunks)
  • packages/db/src/schema/friendPlayer.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/db/src/schema/friendPlayer.ts
  • packages/db/src/relations/index.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/api/src/routers/match.ts (2)
packages/db/src/client.ts (1)
  • db (23-26)
packages/db/src/zodSchema/index.ts (2)
  • selectSharedLocationSchema (105-105)
  • selectSharedPlayerSchema (75-75)
packages/api/src/routers/sharing/share-request.ts (1)
packages/db/src/zodSchema/index.ts (2)
  • selectSharedLocationSchema (105-105)
  • selectSharedPlayerSchema (75-75)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint
🔇 Additional comments (5)
packages/api/src/routers/match.ts (2)

1511-1544: Well-structured function signature with comprehensive type definitions.

The shareMatchWithFriends function has a clear signature with properly typed parameters that document the expected input data structure.


1545-1546: Good use of nested transactions for isolation.

Using a nested transaction for each friend's sharing operation is a solid approach that provides isolation between different friend shares.

packages/api/src/routers/sharing/share-request.ts (3)

68-178: Great improvement using transactions for atomicity.

The refactoring of the requestShareGame mutation to use a database transaction is a significant improvement. It ensures that all database operations either succeed together or fail together, preventing partial completion issues.


1246-1261: Good transaction usage in requestSharePlayer.

This function has been properly refactored to use a database transaction, ensuring all operations are atomic.


1400-1418: Improved return structure includes detailed success messages.

The updated return values from share operations include detailed success/failure messages per friend, which is a great improvement for client-side feedback.

…maintainability

- Refactor sharing functionality into dedicated helper functions for handling location and match sharing, enhancing code organization and readability.
- Streamline transaction management across share requests, ensuring consistent handling of permissions and statuses based on friend settings.
- Improve validation and error handling during sharing operations, providing clearer feedback for users and reducing complexity in the main router logic.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/api/src/routers/match.ts (3)

1508-1609: Complex sharing logic would benefit from smaller functions.

While functionally correct, the shareMatchWithFriends function coordinates multiple operations (share requests, location sharing, game sharing, match creation, player sharing) within nested transactions. Breaking this down into smaller, more focused functions would improve testability and maintainability.

Consider restructuring to further divide responsibilities:

async function shareMatchWithFriends(
  transaction: TransactionType,
  userId: number,
  createdMatch: {...},
  shareFriends: {...}[],
) {
  for (const shareFriend of shareFriends) {
    await transaction.transaction(async (tx) => {
-     let returnedSharedLocation: z.infer<typeof selectSharedLocationSchema> | null = null;
-     const [newShare] = await tx
-       .insert(shareRequest)
-       .values({
-         ownerId: userId,
-         sharedWithId: shareFriend.friendUserId,
-         itemType: "match",
-         itemId: createdMatch.id,
-         status: shareFriend.autoAcceptMatches ? "accepted" : "pending",
-         permission: shareFriend.defaultPermissionForMatches,
-         expiresAt: null,
-       })
-       .returning();
-     if (!newShare) {
-       throw new TRPCError({...});
-     }
+     // Create primary share request for the match
+     const newShare = await createShareRequest(tx, {
+       ownerId: userId,
+       sharedWithId: shareFriend.friendUserId,
+       itemType: "match",
+       itemId: createdMatch.id,
+       status: shareFriend.autoAcceptMatches ? "accepted" : "pending",
+       permission: shareFriend.defaultPermissionForMatches,
+     });
      
      // Rest of the function with similar modularization
    });
  }
}

// Helper that encapsulates creating a share request with error handling
async function createShareRequest(tx: TransactionType, shareData: {...}) {
  const [newShare] = await tx.insert(shareRequest).values({...}).returning();
  if (!newShare) {
    throw new TRPCError({
      code: "INTERNAL_SERVER_ERROR",
      message: "Failed to generate share.",
    });
  }
  return newShare;
}

1610-1672: Add JSDoc comments to document function parameters.

The handleLocationSharing function is well-structured but lacks documentation about its parameters and return value. Adding JSDoc would make its usage clearer, especially given the complexity of the friend setting object.

Add JSDoc documentation:

/**
 * Handles sharing a location with a friend, creating necessary requests and shared entities
 * 
 * @param transaction - The database transaction
 * @param ownerId - ID of the user sharing the location
 * @param locationId - ID of the location being shared
 * @param shareFriend - Object containing friend's sharing preferences
 * @param newShareId - ID of the parent share request
 * @returns The created shared location if auto-accepted, or null
 */
async function handleLocationSharing(...)

1774-1801: Separate concerns in the player sharing logic.

The handlePlayerSharing function combines multiple responsibilities: creating share requests, managing shared player records, and creating shared match player records. This makes the function harder to test and maintain.

Consider splitting this into separate functions:

async function handlePlayerSharing(...) {
  // Only handle the player sharing part
  const returnedSharedPlayer = await createOrFindSharedPlayer(...);
  
  // Delegate match player creation to another function
  if (returnedSharedMatch) {
    await createSharedMatchPlayer(...);
  }
}

async function createOrFindSharedPlayer(...) {
  // Logic to create share request and manage shared player record
}

async function createSharedMatchPlayer(...) {
  // Logic to create shared match player record
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 024d012 and b31914e.

📒 Files selected for processing (3)
  • packages/api/src/routers/match.ts (4 hunks)
  • packages/api/src/routers/sharing/share-request.ts (9 hunks)
  • packages/db/src/client.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/db/src/client.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/api/src/routers/match.ts (2)
packages/db/src/client.ts (1)
  • TransactionType (29-29)
packages/db/src/zodSchema/index.ts (2)
  • selectSharedLocationSchema (105-105)
  • selectSharedPlayerSchema (75-75)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint
🔇 Additional comments (9)
packages/api/src/routers/match.ts (3)

327-343: Good addition of proper error handling for failed match retrieval.

The code correctly fetches the created match with all necessary relations and throws an appropriate error if the match isn't found, ensuring robustness in the auto-sharing feature.


350-415: Comprehensive implementation of friend-aware match sharing.

The code successfully identifies friend relationships among match players and filters them based on sharing preferences. The implementation:

  1. Efficiently queries friendPlayer records with all necessary relations
  2. Applies appropriate filtering based on both sender and recipient sharing preferences
  3. Constructs a detailed permission object for each eligible friend

This approach ensures that sharing respects both users' preferences before attempting any database operations.


417-422: Well-structured approach to share propagation.

Delegating the actual sharing work to a helper function keeps the main mutation clean and focused on match creation, while passing all necessary context for the sharing operation.

packages/api/src/routers/sharing/share-request.ts (6)

69-79: Improved transaction handling for database operations.

The code now consistently uses transactions for all database operations in the sharing process, which ensures atomicity and prevents partial mutations in case of errors.


185-205: Good refactoring of friend sharing logic.

Moving the complex friend sharing logic to a separate helper function (requestShareGameToFriend) improves readability and maintainability of the main mutation. The approach of collecting and returning detailed success/failure messages provides good UX feedback.


365-419: Well-structured permission validation before sharing.

The code properly validates friendship existence and sharing permissions through the reusable validateFriendSharingPermissions helper before proceeding with sharing operations. This ensures robust permission checks and clear error messaging.


1902-1939: Good extraction of permission validation logic.

The validateFriendSharingPermissions function is well-designed, focused on a single responsibility, and provides clear success/failure results with descriptive messages. This improves code reuse and simplifies the calling functions.


1944-2047: Well-documented location sharing helper.

The handleLocationSharing function includes good documentation about its purpose and return value. The function is well-structured with clear logic for checking existing shares, creating share requests, and handling auto-acceptance.


2048-2155: Thorough error handling in match sharing.

The handleMatchSharing function includes comprehensive error handling, particularly when dealing with the shared game dependency. It correctly throws descriptive TRPCErrors when critical operations fail, which will help with debugging and client error messaging.

Gravender added 2 commits May 12, 2025 18:03
… permission handling

- Introduce a new function `handleLocationSharing` to manage the sharing of locations with friends, allowing for auto-acceptance and permission settings.
- Enhance transaction management for share requests related to locations, ensuring proper validation and error handling.
- Streamline the process of checking existing share requests and shared locations, improving overall clarity and maintainability of the sharing logic.
…g for matches and locations

- Introduce a new `sharedMatchWithFriends` function to streamline the sharing process for matches, including location and player sharing based on friend settings.
- Refactor `handleLocationSharing` to improve clarity and maintainability, ensuring proper permission handling and transaction management.
- Update transaction logic to enhance error handling and validation during share requests, improving overall user experience and feedback.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/api/src/routers/match.ts (1)

1614-1674: 🛠️ Refactor suggestion

Add return type annotation to handleGameSharing

The function has an inconsistent return pattern (returns a shared game object in some cases and null in others) without a proper type annotation. This could lead to runtime errors if consumer code assumes a consistent return type.

Add a proper return type annotation:

- async function handleGameSharing(
+ async function handleGameSharing(
  transaction: TransactionType,
  ownerId: number,
  gameId: number,
  shareFriend: {...},
  newShareId: number,
- ) {
+ ): Promise<z.infer<typeof selectSharedGameSchema> | null> {
  // ...function implementation...
}
🧹 Nitpick comments (9)
packages/api/src/utils/sharing.ts (1)

93-117: Consider consolidating duplicate location query parameters.

There's a duplicate query pattern between lines 64-66 and 94-95 for finding a shared location.

- const existingShared = await transaction.query.sharedLocation.findFirst({
-  where: { locationId, sharedWithId: friendId, ownerId },
- });
+ // Create a reusable function for getting shared location
+ async function getSharedLocation(tx: TransactionType, locId: number, friendId: number, ownerId: number) {
+   return tx.query.sharedLocation.findFirst({
+     where: { locationId: locId, sharedWithId: friendId, ownerId },
+   });
+ }
+ 
+ const existingShared = await getSharedLocation(transaction, locationId, friendId, ownerId);
packages/api/src/routers/match.ts (4)

351-374: Complex query to determine friend eligibility for sharing.

The query to find friend players includes multiple nested relations to access both the friend's settings and their reciprocal settings toward the current user. This is necessary for proper permission validation but introduces complexity.

Consider extracting this query to a separate function to improve readability:

async function getFriendPlayersForSharing(db: any, userId: number, playerIds: number[]) {
  return db.query.friendPlayer.findMany({
    where: {
      createdById: userId,
      friendId: {
        in: playerIds,
      },
    },
    with: {
      friend: {
        with: {
          friendSetting: true,
          friend: {
            with: {
              friends: {
                where: { friendId: userId },
                with: { friendSetting: true },
              },
            },
          },
        },
      },
    },
  });
}

375-417: Complex filter logic to determine friends eligible for sharing.

This code segment filters and maps friend players to determine who should receive shared content based on their settings. It's complex but necessary to enforce proper sharing restrictions.

Consider adding a comment explaining the key conditions for auto-sharing eligibility to make the logic more clear to future maintainers:

+ // Auto-share matches with friends when:
+ // 1. The friend has enabled auto-sharing matches (autoShareMatches)
+ // 2. The friend allows receiving shared matches (allowSharedMatches)

1675-1714: Consider adding error handling for edge cases in createSharedMatch.

The function correctly throws an error if the shared match creation fails, but doesn't handle potential edge cases like duplicate shared matches.

Consider adding a check for existing shared matches before creation:

async function createSharedMatch(
  transaction: TransactionType,
  ownerId: number,
  matchId: number,
  shareFriend: {...},
  sharedGameId: number,
  sharedLocationId: number | undefined,
) {
+  // Check for existing shared match to avoid duplicates
+  const existingSharedMatch = await transaction.query.sharedMatch.findFirst({
+    where: {
+      ownerId,
+      sharedWithId: shareFriend.friendUserId,
+      matchId,
+      sharedGameId,
+    },
+  });
+  
+  if (existingSharedMatch) {
+    return existingSharedMatch;
+  }

  const [createdSharedMatch] = await transaction
    .insert(sharedMatch)
    .values({
      // ... existing code
    })
    .returning();
  
  // ... remaining code
}

1715-1806: Consider refactoring handlePlayerSharing for readability

The function is lengthy and handles multiple operations (share request creation, shared player creation, shared match player creation). This complexity makes it harder to maintain and test.

Consider breaking this function down into smaller helper functions, each with a single responsibility:

async function handlePlayerSharing(...) {
  // Get or create shared player if settings allow
  const returnedSharedPlayer = shareFriend.sharePlayers && shareFriend.allowSharedPlayers
    ? await getOrCreateSharedPlayer(transaction, userId, matchPlayer, shareFriend, newShareId)
    : null;
    
  // Create shared match player if match was shared
  if (returnedSharedMatch && shareFriend.autoAcceptMatches) {
    await createSharedMatchPlayer(
      transaction, 
      userId, 
      matchPlayer, 
      shareFriend, 
      returnedSharedMatch, 
      returnedSharedPlayer
    );
  }
}

// Additional helper functions...
packages/api/src/routers/sharing/share-request.ts (4)

364-740: Complex friend sharing logic could benefit from additional modularization

While the code is now more organized than before, this match sharing implementation is still quite long (376 lines) and handles multiple concerns. Consider further modularization by extracting functionality for shared scoresheets, players, and game sharing into separate helpers similar to how you've done with handleLocationSharing.

For example, you could create helpers like:

// Extract scoresheet sharing logic
async function handleScoresheetSharing(
  transaction: TransactionType,
  userId: number,
  friendId: number,
  scoresheetId: number,
  sharedGameId: number,
  autoAccept: boolean,
  permission: "view" | "edit",
  parentShareId: number,
  expiresAt: Date | null
) {
  // Implementation...
}

// Extract player sharing logic
async function handlePlayerSharing(
  transaction: TransactionType,
  userId: number,
  friendId: number,
  playerId: number,
  autoAccept: boolean,
  permission: "view" | "edit",
  parentShareId: number,
  expiresAt: Date | null
) {
  // Implementation...
}

1118-1118: Commented code intention without implementation

There's a comment indicating an intention to check if a match is already shared, but there's no implementation following it. Either implement the check or remove the comment to avoid confusion.

-                //check if the match is already shared with the user or the previous share request has expired
+                // Check if the match is already shared
+                const existingMatchShare = await hasExistingShare(
+                  tx2,
+                  userId,
+                  friendToShareTo.id,
+                  "match",
+                  matchToShare.matchId
+                );
+                
+                if (existingMatchShare) {
+                  shareMessages.push({
+                    success: false,
+                    message: "Match already shared or pending",
+                  });
+                  return false;
+                }

1709-1712: Share request expiration logic could be more flexible

The current implementation only checks for pending shares created within the last 7 days. Consider making this duration configurable or moving it to a constant at the top of the file for easier maintenance.

+// At the top of the file
+const PENDING_SHARE_EXPIRY_DAYS = 7;

// Then in the function
-          createdAt: {
-            gt: subDays(new Date(), 7),
-          },
+          createdAt: {
+            gt: subDays(new Date(), PENDING_SHARE_EXPIRY_DAYS),
+          },

2113-2176: Scoresheet sharing could be optimized for performance

The shareScoresheetsWithFriend function performs separate database queries for each scoresheet. Consider optimizing this by batching queries where possible or implementing bulk operations.

For example, you could fetch all existing shared scoresheets in a single query:

// Fetch all existing shared scoresheets in one query
const existingSharedScoresheets = await transaction.query.sharedScoresheet.findMany({
  where: {
    scoresheetId: {
      in: scoreSheets.map(s => s.scoresheetId)
    },
    sharedWithId: friendId,
    ownerId: userId
  }
});

// Create a Set for O(1) lookups
const existingScoreSheetIds = new Set(
  existingSharedScoresheets.map(s => s.scoresheetId)
);

// Then check against this Set in your loop
for (const scoresheetToShare of scoreSheets) {
  // ... existing code ...
  
  if (autoAcceptGame) {
    const alreadyShared = existingScoreSheetIds.has(scoresheetToShare.scoresheetId);
    if (!alreadyShared && returnedSharedGame) {
      // Create shared scoresheet...
    }
  }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b31914e and 6c26ceb.

📒 Files selected for processing (3)
  • packages/api/src/routers/match.ts (5 hunks)
  • packages/api/src/routers/sharing/share-request.ts (9 hunks)
  • packages/api/src/utils/sharing.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/api/src/utils/sharing.ts (1)
packages/db/src/client.ts (1)
  • TransactionType (29-29)
packages/api/src/routers/sharing/share-request.ts (3)
packages/db/src/zodSchema/index.ts (2)
  • selectSharedLocationSchema (105-105)
  • selectSharedPlayerSchema (75-75)
packages/api/src/utils/sharing.ts (1)
  • handleLocationSharing (29-118)
packages/db/src/client.ts (1)
  • TransactionType (29-29)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint
🔇 Additional comments (22)
packages/api/src/utils/sharing.ts (5)

6-23: Well-designed interface for friend sharing settings.

The FriendSharingSettings interface is well-structured and comprehensive, covering all necessary preferences for granular sharing control. This aligns perfectly with the PR's objective to introduce per-friend sharing preferences.


29-38: Good function signature with clear typing and documentation.

The function signature for handleLocationSharing is well-defined with appropriate parameter types and default values. The TypeScript typing is thorough, and the JSDoc comment explains the purpose and return value clearly.


40-59: Efficient query for existing share requests.

The query efficiently checks for both pending and accepted share requests in a single database call using the OR condition, which helps minimize database round trips.


61-76: Verify that auto-accept logic always finds a corresponding shared location record.

When an existing share request is found and auto-accept is enabled, the code assumes a shared location record exists. However, there's a potential edge case where a share request could exist with status "accepted" but the corresponding shared location record was not created or was deleted.

Consider strengthening the recovery logic here to handle this edge case by creating a new shared location record if one is not found.


78-91: Transaction usage ensures atomicity of share request creation.

The use of the transaction object for inserting the share request ensures that the operation is atomic with other database operations in the same transaction, which is essential for data integrity.

packages/api/src/routers/match.ts (3)

7-44: Good import organization with type imports separated.

The imports are organized logically, with type imports separated from regular imports. The use of the import type syntax for type-only imports is a good practice for TypeScript as it can help with compilation performance.


328-350: Comprehensive match data retrieval for sharing preparation.

The query for retrieving the created match includes all necessary related data (scoresheet, game, matchPlayers, location) needed for the sharing process, which avoids additional database queries later.


1509-1612: Well-structured function for sharing match with friends.

The shareMatchWithFriends function clearly separates the sharing of different entities (match, location, game, players) and properly handles transaction management. The nested transaction pattern ensures atomicity for each friend's sharing operation.

packages/api/src/routers/sharing/share-request.ts (14)

6-12: Clean use of TypeScript imports with specific types

The import statements clearly specify the exact types needed from the database schemas, which is a good practice for maintaining type safety and keeping the code maintainable.


69-79: Correctly wrapped database operations in transactions

You've properly wrapped all database operations within a transaction to ensure atomicity, which is crucial for maintaining data consistency when multiple operations need to succeed or fail together.


80-178: The link sharing logic has been simplified and made consistent

The refactored link sharing implementation consistently uses transactions for all database operations, making the code more reliable and maintainable.


179-206: Good extraction of friend sharing logic into a helper function

The delegation of friend sharing logic to a dedicated helper function (requestShareGameToFriend) significantly improves code organization and addresses the previous feedback about modularizing this complex logic.


444-456: Good reuse of the shared location handling utility

You've properly reused the handleLocationSharing utility function, which follows the DRY principle and ensures consistent behavior across different parts of the application.


483-614: Consistent auto-acceptance handling with appropriate verification

This section correctly implements auto-acceptance logic for games and scoresheets, with appropriate verification of existing shares to prevent duplicates. The code properly separates share request creation from shared entity creation.


938-942: Correct error message for missing scoresheets

Here, the error message correctly matches the condition - throwing an error when no game scoresheets are found.


1551-1654: Well-structured helper function for game sharing

The requestShareGameToFriend function is well-structured and handles all the necessary concerns for sharing a game with a friend. It correctly uses nested transactions, validates permissions, and delegates to other helper functions for specific tasks.


1655-1692: Clear validation of friend sharing permissions

This helper function cleanly separates the validation logic for checking if sharing is allowed based on user preferences and friendship status. The function has a clear return signature that indicates success or failure with an appropriate message.


1693-1724: Reusable utility to check for existing shares

The hasExistingShare function encapsulates the logic for checking if an item is already shared, which is used across multiple sharing operations. This reduces code duplication and ensures consistent behavior.


1802-2004: Comprehensive match sharing implementation

The sharedMatchWithFriends function provides a complete implementation for sharing matches, including location sharing, player sharing, and appropriate permission handling. The use of early returns with error messages keeps the code clean and maintainable.


2005-2112: Well-structured match sharing handler with appropriate error checking

The handleMatchSharing function contains appropriate null checks and error handling, ensuring that shared entities are created consistently and with proper references.


24-24: Good integration with shared utility functions

Importing the handleLocationSharing function from a shared utility file is a good practice that promotes code reuse across different parts of the application.


1344-1354: Efficient use of the handleMatchSharing helper

This segment properly delegates to the handleMatchSharing helper function, passing all required parameters for creating shared match entries. The function maintains the hierarchical relationship between games, matches, locations, and players.

…ing and transaction management

- Refactor match sharing functionality to streamline the process of sharing matches and players with friends based on their settings.
- Introduce a new `createOrFindSharedPlayer` function to handle player sharing more effectively, ensuring proper permission assignment and transaction management.
- Update error handling and validation during share requests, improving user feedback and overall clarity in the sharing process.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/api/src/routers/match.ts (1)

1616-1676: 🛠️ Refactor suggestion

Add explicit return type for better type safety.

The handleGameSharing function has an inconsistent return pattern. It returns a shared game object in some cases and null in others without a clear type definition.

async function handleGameSharing(
  transaction: TransactionType,
  ownerId: number,
  gameId: number,
  shareFriend: {
    friendUserId: number;
    shareLocation: boolean;
    sharePlayers: boolean;
    defaultPermissionForMatches: "view" | "edit";
    defaultPermissionForPlayers: "view" | "edit";
    defaultPermissionForLocation: "view" | "edit";
    defaultPermissionForGame: "view" | "edit";
    allowSharedPlayers: boolean;
    allowSharedLocation: boolean;
    autoAcceptMatches: boolean;
    autoAcceptPlayers: boolean;
    autoAcceptLocation: boolean;
  },
  newShareId: number,
+): Promise<z.infer<typeof selectSharedGameSchema> | null> {
packages/api/src/routers/sharing/share-request.ts (1)

939-943: ⚠️ Potential issue

Fix error message to match the condition.

The error message indicates "No games scoresheets found" but the condition is checking for an existing share game request. This mismatch could confuse developers maintaining the code.

-                throw new TRPCError({
-                  code: "INTERNAL_SERVER_ERROR",
-                  message: "No games scoresheets found",
-                });
+                throw new TRPCError({
+                  code: "INTERNAL_SERVER_ERROR",
+                  message: "Expected game scoresheets not found",
+                });
🧹 Nitpick comments (4)
packages/api/src/routers/match.ts (3)

1677-1716: Add explicit return type definition for createSharedMatch.

The function logic is sound, but adding an explicit return type would improve type safety and code clarity.

async function createSharedMatch(
  transaction: TransactionType,
  ownerId: number,
  matchId: number,
  shareFriend: {
    friendUserId: number;
    shareLocation: boolean;
    sharePlayers: boolean;
    defaultPermissionForMatches: "view" | "edit";
    defaultPermissionForPlayers: "view" | "edit";
    defaultPermissionForLocation: "view" | "edit";
    defaultPermissionForGame: "view" | "edit";
    allowSharedPlayers: boolean;
    allowSharedLocation: boolean;
    autoAcceptMatches: boolean;
    autoAcceptPlayers: boolean;
    autoAcceptLocation: boolean;
  },
  sharedGameId: number,
  sharedLocationId: number | undefined,
+): Promise<z.infer<typeof selectSharedMatchSchema>> {

1717-1766: Optimize player sharing with bulk operations where possible.

The function correctly handles player sharing, creating share requests and shared entities as needed. For performance optimization with many players, consider implementing bulk inserts rather than iterating through each player individually.


1833-1881: Consider documenting the permissions propagation model.

The createSharedMatchPlayer function correctly sets up the relationship between shared matches, players, and match players with appropriate permissions. Consider adding documentation comments to clarify how permissions propagate through the sharing hierarchy.

+/**
+ * Creates a shared match player entity, linking a shared match with a player.
+ * Permissions are inherited from the match-level permissions when the shared player
+ * doesn't exist, ensuring consistent permission propagation throughout the sharing hierarchy.
+ */
async function createSharedMatchPlayer(
packages/api/src/routers/sharing/share-request.ts (1)

1818-2020: Consider further decomposing this lengthy function.

The sharedMatchWithFriends function is quite long (200+ lines) and handles multiple responsibilities. While it works correctly, it could be decomposed into smaller functions for better maintainability and testability.

For example, you could extract the player sharing logic (lines 1939-2017) into a separate function:

async function handleMatchPlayerSharing(
  transaction: TransactionType,
  userId: number,
  matchPlayers: any[],
  friendId: number,
  parentShare: { id: number; permission: "view" | "edit"; expiresAt: Date | null },
  returnedShareMatch: any | null,
  friendSettings: any | undefined,
) {
  // Player sharing logic here
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c26ceb and e717072.

📒 Files selected for processing (2)
  • packages/api/src/routers/match.ts (5 hunks)
  • packages/api/src/routers/sharing/share-request.ts (9 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/api/src/routers/match.ts (3)
packages/db/src/client.ts (1)
  • TransactionType (29-29)
packages/db/src/zodSchema/index.ts (1)
  • selectSharedLocationSchema (105-105)
packages/api/src/utils/sharing.ts (1)
  • handleLocationSharing (29-118)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint
  • GitHub Check: typecheck
🔇 Additional comments (8)
packages/api/src/routers/match.ts (2)

1511-1514: Well-structured match sharing implementation.

The shareMatchWithFriends function follows a good pattern of orchestrating the sharing process for multiple friends. The function signature is clear and the implementation handles permissions and auto-acceptance logic correctly.


1589-1601: ⚠️ Potential issue

Add a null check before using returnedSharedGame in createSharedMatch.

There's a potential issue if handleGameSharing returns null but then createSharedMatch is called with an expectation that it returned a valid object.

let returnedSharedMatch: z.infer<typeof selectSharedMatchSchema> | null = null;
- if (shareFriend.autoAcceptMatches && returnedSharedGame) {
+ // Ensure we have a valid shared game before creating a shared match
+ if (shareFriend.autoAcceptMatches && returnedSharedGame && returnedSharedGame.id) {
  returnedSharedMatch = await createSharedMatch(
    tx,
    userId,
    createdMatch.id,
    shareFriend,
    returnedSharedGame.id,
    returnedSharedLocation?.id ?? undefined,
  );
}

Likely an incorrect or invalid review comment.

packages/api/src/routers/sharing/share-request.ts (6)

1567-1670: Well-structured helper for game sharing with friends.

The requestShareGameToFriend function properly encapsulates the complex logic for sharing games with friends, including validation, permission checks, and handling of child entities like matches and scoresheets.


1671-1708: Good separation of concerns for validation logic.

The validateFriendSharingPermissions function is well-designed to separate validation logic from the sharing implementation, making it reusable across different sharing operations.


1709-1740: Consistent approach for checking existing shares.

The hasExistingShare function provides a standardized way to check for existing shares, reducing code duplication and ensuring consistent behavior across different sharing operations.


2129-2192: Well-structured scoresheet sharing implementation.

The shareScoresheetsWithFriend function follows a clean pattern of checking for existing shares, creating share requests, and creating shared entities if auto-accept is enabled.


69-79: Consistent use of transactions improves data integrity.

The refactored code correctly uses a transaction for all database operations, ensuring atomicity and consistency when sharing entities.


2021-2128: Good error handling in match sharing logic.

The handleMatchSharing function includes comprehensive error handling for cases where required shared entities (like shared games) are missing, preventing inconsistent sharing states.

Gravender added 2 commits May 12, 2025 19:43
- Rename `linkedPlayers` to `linkedPlayer` and adjust the relationship to reference `createdBy` instead of `userId`.
- Introduce filtering conditions for linked players to ensure only active users are included.
- Remove the `friendPlayer` schema and its references from the database, streamlining the player relationships.
- Update player schema to include `isUser` and `friendId` fields for better relationship management.
- Modify seeding logic to set `isUser` for players created by users, enhancing data integrity.
- Update various routers to replace `userId` checks with the new `isUser` flag for improved clarity and consistency in user identification.
- Adjust player and match handling logic to utilize the `isUser` property, enhancing data integrity and simplifying user-related queries.
- Ensure all relevant areas in the codebase reflect this change, streamlining the user identification process across the application.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
packages/api/src/routers/friends.ts (1)

164-176: clerkClient should not be called – the SDK object is already initialised
This was flagged in a previous review but is still present. Calling await clerkClient() throws TypeError: clerkClient is not a function.

-const client = await clerkClient();
+const client = clerkClient;
🧹 Nitpick comments (5)
apps/nextjs/src/app/dashboard/friends/[id]/page.tsx (1)

25-27: Added Open Graph metadata for better social sharing.

This enhancement improves SEO and social media sharing by adding the friend's image URL to the metadata. Consider adding a null check for friend.imageUrl to prevent errors if it's undefined.

    openGraph: {
-      images: [friend.imageUrl],
+      images: friend.imageUrl ? [friend.imageUrl] : [],
    },
apps/nextjs/src/components/breadcrumbs.tsx (2)

26-35: useQuery loses type-safety by casting any – let TRPC/React-Query infer it automatically

useQuery(trpc.dashboard.getBreadCrumbs.queryOptions(/* … */)) already returns a typed hook, so the subsequent cast at line 36 is unnecessary and masks type errors.

-const data = BreadCrumbsQuery.data as
-  | RouterOutputs["dashboard"]["getBreadCrumbs"]
-  | undefined;
+const data = BreadCrumbsQuery.data;

40-62: Duplicate URL parsing logic – extract to a helper for readability & DRY

The fallback branch repeats the same paths.split("/").filter(...).slice(...) pattern three times, which is brittle and hard to read. Consider moving the transformation into a dedicated utility:

function splitPath(pathname: string) {
  const parts = pathname.split("/").filter(Boolean);
  return parts.map((_p, i) => ({
    name: parts[i]
      .replace(/-/g, " ")
      .replace(/\b\w/g, (c) => c.toUpperCase()),
    path: "/" + parts.slice(0, i + 1).join("/"),
  }));
}

Then simply use splitPath(paths).slice(1) in the JSX.

packages/api/src/routers/friends.ts (2)

573-576: Use compareDesc (or simpler multiplier) for descending sort

compareAsc(b.date, a.date) works but is obscure; compareDesc(a.date, b.date) (or (b.date as any) - (a.date as any) ) conveys intent directly.

-allMatches.sort((a, b) => compareAsc(b.date, a.date));
+allMatches.sort((a, b) => compareDesc(a.date, b.date));

599-607: Initial winRate calculation should mirror the later update logic

The update path sets g.winRate = g.wins / g.plays, but the initial insertion stores winRate: isWin (0 or 1). This is correct numerically but obscures intent and requires mental gymnastics.

-            winRate: isWin,
+            winRate: isWin / 1, // explicit ratio for consistency
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fa8be1 and fc1e2c7.

📒 Files selected for processing (9)
  • .vscode/settings.json (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx (3 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/shared-game-card.tsx (3 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/shared-player-card.tsx (2 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/page.tsx (2 hunks)
  • apps/nextjs/src/components/breadcrumbs.tsx (2 hunks)
  • packages/api/src/root.ts (0 hunks)
  • packages/api/src/routers/dashboard.ts (5 hunks)
  • packages/api/src/routers/friends.ts (6 hunks)
💤 Files with no reviewable changes (1)
  • packages/api/src/root.ts
✅ Files skipped from review due to trivial changes (1)
  • .vscode/settings.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/api/src/routers/dashboard.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
apps/nextjs/src/app/dashboard/friends/[id]/_components/shared-player-card.tsx (1)
apps/nextjs/src/app/dashboard/settings/_components/uploadBGGdata.tsx (1)
  • Player (118-126)
apps/nextjs/src/app/dashboard/friends/[id]/_components/shared-game-card.tsx (2)
apps/expo/src/utils/api.tsx (1)
  • RouterOutputs (57-57)
apps/nextjs/src/app/dashboard/settings/_components/uploadBGGdata.tsx (1)
  • Game (49-76)
packages/api/src/routers/friends.ts (2)
packages/api/src/utils/sharing.ts (2)
  • collectShares (172-309)
  • mapSharedMatchPlayers (311-398)
packages/api/src/trpc.ts (1)
  • protectedUserProcedure (182-182)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint
🔇 Additional comments (11)
apps/nextjs/src/app/dashboard/friends/[id]/page.tsx (2)

6-6: Corrected import path spelling.

This fix resolves the typo in the directory name from "componenets" to "components".


23-23:

✅ Verification successful

Check for null or undefined friend name.

The fallback value that was previously present (?? "Friend") has been removed. Ensure that friend.name is always defined, or consider adding a fallback.


🏁 Script executed:

#!/bin/bash
# Check if there are any null checks for friend.name in the codebase
rg "friend\.name\s*\?\?" --type=typescript

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Search for any remaining fallback usages of friend.name (e.g., ?? "…")
rg -e 'friend\.name\s*\?\?' -g '*.ts' -g '*.tsx'

# Search for optional chaining usage on friend.name
rg -e 'friend\?\.name' -g '*.ts' -g '*.tsx'

Length of output: 573


🏁 Script executed:

#!/bin/bash
# Display the first 200 lines of the page to inspect how `friend` is obtained and typed
sed -n '1,200p' apps/nextjs/src/app/dashboard/friends/\[id\]/page.tsx

# Find where `getFriend` is imported and used to see the return type
rg -n "import.*getFriend" apps/nextjs/src/app/dashboard/friends/\[id\]/page.tsx
rg -n "getFriend" apps/nextjs/src/app/dashboard/friends/\[id\]/page.tsx

Length of output: 2223


No fallback needed for friend.name

The generateMetadata call already guards against a missing friend (if (!friend) redirect(...)), and the TRPC getFriendMetaData procedure returns a non-nullable name string. Therefore friend.name is always defined and no ?? "Friend" fallback is required.

apps/nextjs/src/app/dashboard/friends/[id]/_components/shared-player-card.tsx (2)

16-23: Improved type definitions with Extract utility.

The introduction of SharedItem type and use of TypeScript's Extract utility type provides better type safety and more accurate representation of the data structure from the API.


34-34: Updated property access to match new data structure.

The property access has been correctly updated from the nested player.player.name to the direct player.name property, aligning with the flattened data structure from the API.

apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx (2)

60-83: Direct iteration through shared items is more efficient.

The code now directly maps over friend.sharedTo array rather than using intermediate arrays created with useMemo. This simplifies the code and improves performance by eliminating unnecessary array manipulations.

However, add null checks for Clerk user properties in the empty state message:

                <p className="mt-2 text-sm text-muted-foreground">
-                  {`${friend.clerkUser.firstName} ${friend.clerkUser.lastName}`}{" "}
+                  {`${friend.clerkUser?.firstName || ""} ${friend.clerkUser?.lastName || ""}`}{" "}
                  hasn't shared any games or players with you yet
                </p>

87-111: Similar improvement for shared-with-friend section.

The direct use of friend.sharedWith array is a good optimization, similar to the shared-by-friend section.

Add null checks for Clerk user properties in the empty state message here as well:

                  You haven't shared any games or players with{" "}
-                  {`${friend.clerkUser.firstName} ${friend.clerkUser.lastName}`}{" "}
+                  {`${friend.clerkUser?.firstName || ""} ${friend.clerkUser?.lastName || ""}`}{" "}
                  yet
apps/nextjs/src/app/dashboard/friends/[id]/_components/shared-game-card.tsx (5)

16-23: Consistent type pattern with shared-player-card.

Good use of TypeScript's Extract utility type to define the Game type, maintaining consistency with the approach used in shared-player-card.tsx.


34-34: Updated property access to match flattened data structure.

Properly updated from the nested game.game.name to the direct game.name property, reflecting the simplified data structure.


48-53: Updated matches array reference.

The property has been correctly updated from sharedMatches to matches, along with proper pluralization logic.


54-59: Updated scoresheets array reference.

The property has been correctly updated from sharedScoresheets to scoresheets, maintaining consistent pluralization logic.


61-63: Add "Game only" message when no related items are shared.

This provides a clearer user experience by explicitly stating that only the game itself is shared when there are no matches or scoresheets included.

Gravender added 2 commits May 14, 2025 18:18
- Refactor the friends router to utilize a new `mapMatches` utility for improved handling of match data, streamlining the retrieval process for both original and shared matches.
- Simplify the logic for mapping clerk user details to friends, enhancing clarity and maintainability.
- Remove redundant match record definitions and improve the structure of match data retrieval, ensuring a more efficient and readable codebase.
- Update the sharing logic to reflect changes in data structure, enhancing the overall sharing experience.
- Update the friend profile page to use a unified name format for clerk users, improving consistency in display.
- Modify metadata generation to handle cases where the friend's image URL may be absent, ensuring robust metadata creation.
- Streamline breadcrumb component by simplifying query handling and removing unnecessary type definitions, enhancing clarity and performance.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/api/src/utils/game.ts (1)

264-264: Use compareDesc for clarity & intent

You rely on the argument reversal trick (compareAsc(b.date, a.date)) to get descending order.
While it works, it obscures intent for the next maintainer. date-fns exposes compareDesc, which is self-documenting and avoids cognitive overhead.

-import { compareAsc } from "date-fns";
+import { compareDesc } from "date-fns";
 ...
-allMatches.sort((a, b) => compareAsc(b.date, a.date));
+allMatches.sort((a, b) => compareDesc(a.date, b.date));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc1e2c7 and 783b4a1.

📒 Files selected for processing (6)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx (3 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/page.tsx (2 hunks)
  • apps/nextjs/src/components/breadcrumbs.tsx (2 hunks)
  • packages/api/src/routers/friends.ts (8 hunks)
  • packages/api/src/utils/game.ts (1 hunks)
  • packages/api/src/utils/sharing.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/nextjs/src/app/dashboard/friends/[id]/page.tsx
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx
  • apps/nextjs/src/components/breadcrumbs.tsx
  • packages/api/src/utils/sharing.ts
  • packages/api/src/routers/friends.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint
🔇 Additional comments (1)
packages/api/src/utils/game.ts (1)

308-319: Filtering may remove non-friend players unintentionally

filter(p => p.sharedPlayer?.linkedPlayerId === friendPlayerId) only keeps players explicitly linked to the friend, so opponents / team-mates disappear from the players list of shared matches.
If the UI needs the full roster (mirroring behaviour of original matches), drop this filter and instead flag the friend with isUser.

Please confirm whether the consumer expects all players or only the friend’s representation. If the former, we need to adjust the filter logic.

- Update the FriendsPage component to improve layout and responsiveness, utilizing a grid system for better organization of friend lists and requests.
- Introduce skeleton loading components for FriendsList and FriendRequests to enhance user experience during data fetching.
- Refactor FriendCard and FriendRequestCard components to accept structured friend and request data, improving clarity and maintainability.
- Remove the deprecated friend-tab component, consolidating functionality into more focused components for better code organization.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
packages/api/src/utils/game.ts (2)

233-259: ⚠️ Potential issue

Potential duplication of the same shared match

The nested forEach loops can result in duplicate matches being added to allMatches if a match contains multiple sharedMatchPlayers records.

  sharedLinkedPlayers.forEach((lp) => {
+   const seen = new Set<number>();
    lp.sharedMatchPlayers.forEach((smp) => {
+     if (seen.has(smp.sharedMatch.match.id)) return;   // skip duplicates
+     seen.add(smp.sharedMatch.match.id);
      const sm = smp.sharedMatch;
      const gameEntity = sm.sharedGame.linkedGame ?? sm.sharedGame.game;
      const players = mapSharedMatchPlayers(sm.sharedMatchPlayers);
      const locationName = sm.sharedLocation?.linkedLocation
        ? sm.sharedLocation.linkedLocation.name
        : sm.sharedLocation?.location.name;
      allMatches.push({
        type: "Shared",
        id: sm.match.id,
        name: sm.match.name,
        date: sm.match.date,
        duration: sm.match.duration,
        finished: sm.match.finished,
        gameId: gameEntity.id,
        gameName: gameEntity.name,
        locationName: locationName ?? undefined,
        players,
        outcome: {
          score: smp.matchPlayer.score,
          isWinner: smp.matchPlayer.winner ?? false,
          placement: smp.matchPlayer.placement,
        },
      });
    });
  });

302-324: ⚠️ Potential issue

Loss of type-safety after filtering nulls

TypeScript doesn't automatically narrow the type after filtering out null values without a type predicate. Fix this by adding an explicit type predicate to the filter.

  return sharedMatchPlayers
    .map((p) => {
      const linkedPlayer = p.sharedPlayer?.linkedPlayer;
      if (linkedPlayer) {
        return {
          playerId: linkedPlayer.id,
          name: linkedPlayer.name,
          score: p.matchPlayer.score,
          isWinner: p.matchPlayer.winner ?? false,
          placement: p.matchPlayer.placement,
        };
      } else if (p.sharedPlayer) {
        return {
          playerId: p.sharedPlayer.playerId,
          name: p.sharedPlayer.player.name,
          score: p.matchPlayer.score,
          isWinner: p.matchPlayer.winner ?? false,
          placement: p.matchPlayer.placement,
        };
      }
      return null;
    })
-   .filter((p) => p !== null);
+   .filter(
+     (p): p is NonNullable<typeof p> => p !== null,
+   );
packages/api/src/routers/friends.ts (2)

712-722: ⚠️ Potential issue

Fix the clerkClient usage

clerkClient is an object, not a function, so it shouldn't be called with parentheses.

- const client = await clerkClient();
+ const client = clerkClient;

  const clerkUser = await client.users
    .getUser(returnedFriend.friend.clerkUserId)
    .catch((error) => {
      console.error(error);
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "Friend not found",
      });
    });

865-875: ⚠️ Potential issue

Fix clerkClient usage in getFriendMetaData

This is the same issue as above - clerkClient is an object, not a function.

- const client = await clerkClient();
+ const client = clerkClient;

  const clerkUser = await client.users
    .getUser(returnedFriend.friend.clerkUserId)
    .catch((error) => {
      console.error(error);
      throw new TRPCError({
        code: "NOT_FOUND",
        message: "Friend not found",
      });
    });
🧹 Nitpick comments (6)
apps/nextjs/src/app/dashboard/friends/_components/friend-requests-skeleton.tsx (1)

27-57: Consider implementing skeleton for both tabs.

The skeleton only implements the "Received" tab content and not the "Sent" tab. For a more complete loading experience, consider adding skeleton placeholders for the "Sent" tab as well.

+      <TabsContent value="sent">
+        <Card>
+          <CardHeader>
+            <CardTitle>Sent Requests</CardTitle>
+            <CardDescription>Friend requests you've sent</CardDescription>
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-4">
+              {Array(2)
+                .fill(0)
+                .map((_, i) => (
+                  <div key={i} className="rounded-lg border p-4">
+                    <div className="flex items-center gap-4">
+                      <Skeleton className="h-10 w-10 rounded-full" />
+                      <div className="flex-grow space-y-2">
+                        <Skeleton className="h-4 w-32" />
+                        <Skeleton className="h-3 w-24" />
+                      </div>
+                      <div className="flex gap-2">
+                        <Skeleton className="h-8 w-8 rounded-full" />
+                      </div>
+                    </div>
+                  </div>
+                ))}
+            </div>
+          </CardContent>
+        </Card>
+      </TabsContent>
apps/nextjs/src/app/dashboard/friends/_components/friend-card.tsx (1)

76-96: Enhanced user profile display with accessibility improvements.

The updated layout with Avatar component and additional user details (username/email) provides a more comprehensive and visually appealing display. Consider adding an aria-label to the link for better accessibility.

<Link
  prefetch={true}
  href={`/dashboard/friends/${friend.id}`}
  className="flex items-center gap-4"
+ aria-label={`View ${friend.name}'s profile`}
>
apps/nextjs/src/app/dashboard/friends/_components/friend-requests-tabs.tsx (1)

67-80: Add pagination for potentially large request lists.

The scrollable area with fixed max height is a good start, but consider implementing pagination for better performance with large numbers of friend requests.

// Example pagination implementation
const [page, setPage] = useState(1);
const pageSize = 10;
const paginatedRequests = pendingRequests.slice((page - 1) * pageSize, page * pageSize);

// In the JSX, render paginatedRequests instead of pendingRequests
// Then add pagination controls below:
<div className="mt-4 flex justify-center">
  <Pagination>
    <PaginationContent>
      <PaginationItem>
        <PaginationPrevious 
          onClick={() => setPage(p => Math.max(1, p - 1))}
          disabled={page === 1} 
        />
      </PaginationItem>
      {/* Page numbers */}
      <PaginationItem>
        <PaginationNext 
          onClick={() => setPage(p => p + 1)}
          disabled={page * pageSize >= pendingRequests.length} 
        />
      </PaginationItem>
    </PaginationContent>
  </Pagination>
</div>
apps/nextjs/src/app/dashboard/friends/_components/friend-list.tsx (2)

28-38: Consider adding TypeScript type for filtered friends

The filteredFriends function uses implicit typing for the friend objects. Adding explicit type definitions would improve type safety and make the component more maintainable.

  const filteredFriends = useMemo(() => {
    const query = searchQuery.toLowerCase();
+   // Define explicit return type
    return friends.filter((friend) => {
      const name = friend.name;
      const email = friend.email ?? "";
      return (
        name.toLowerCase().includes(query) ||
        email.toLowerCase().includes(query)
      );
    });
  }, [friends, searchQuery]);

59-67: Consider adding pagination for large friends lists

For users with many friends, the current implementation loads and displays all friends at once, which could lead to performance issues. Consider implementing pagination or virtual scrolling for better performance.

  {filteredFriends.length > 0 ? (
    <ScrollArea>
-     <div className="grid max-h-[600px] gap-2">
+     <div className="grid max-h-[600px] gap-2">
        {filteredFriends.map((friend) => (
          <FriendCard key={friend.id} friend={friend} />
        ))}
      </div>
    </ScrollArea>
apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx (1)

80-102: Consider extracting reusable success handler logic

The success handlers for all three mutations (accept, reject, cancel) perform the same query invalidation steps. Consider extracting this logic to a reusable function to reduce duplication.

+ // Add at the top of the component
+ const handleSuccessfulMutation = async (message: string) => {
+   toast({ title: message });
+   await queryClient.invalidateQueries(
+     trpc.friend.getFriendRequests.queryOptions(),
+   );
+   await queryClient.invalidateQueries(
+     trpc.friend.getFriends.queryOptions(),
+   );
+   setIsLoading(null);
+ };

  const cancelFriendRequestMutation = useMutation(
    trpc.friend.cancelFriendRequest.mutationOptions({
-     onSuccess: async () => {
-       toast({
-         title: "Friend request canceled",
-       });
-       await queryClient.invalidateQueries(
-         trpc.friend.getFriendRequests.queryOptions(),
-       );
-       await queryClient.invalidateQueries(
-         trpc.friend.getFriends.queryOptions(),
-       );
-       setIsLoading(null);
-     },
+     onSuccess: () => handleSuccessfulMutation("Friend request canceled"),
      onError: (error) => {
        toast({
          title: "Error",
          description: error.message || "Failed to cancel friend request",
          variant: "destructive",
        });
      },
    }),
  );

Then apply similar changes to the other mutations.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 783b4a1 and b318e70.

📒 Files selected for processing (10)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-card.tsx (6 hunks)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-list-skeleton.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-list.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx (2 hunks)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-requests-skeleton.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-requests-tabs.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-tab.tsx (0 hunks)
  • apps/nextjs/src/app/dashboard/friends/page.tsx (3 hunks)
  • packages/api/src/routers/friends.ts (10 hunks)
  • packages/api/src/utils/game.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-tab.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-list-skeleton.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/nextjs/src/app/dashboard/friends/page.tsx (4)
apps/nextjs/src/app/dashboard/friends/_components/friend-list-skeleton.tsx (1)
  • FriendsListSkeleton (14-51)
apps/nextjs/src/app/dashboard/friends/_components/friend-list.tsx (1)
  • FriendsList (21-79)
apps/nextjs/src/app/dashboard/friends/_components/friend-requests-skeleton.tsx (1)
  • FriendRequestsSkeleton (13-60)
apps/nextjs/src/app/dashboard/friends/_components/friend-requests-tabs.tsx (1)
  • FriendRequestsTabs (20-112)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint
  • GitHub Check: typecheck
🔇 Additional comments (15)
apps/nextjs/src/app/dashboard/friends/_components/friend-requests-skeleton.tsx (2)

1-11: Appropriate icon and UI imports for the skeleton component.

Good selection of imports for the skeleton component, matching the icons and UI components that would be used in the actual FriendRequestsTabs component.


13-25: Well-structured tabs header with matching layout to actual component.

The skeleton tabs implementation correctly mirrors the structure in the actual FriendRequestsTabs component, including the grid layout and icon placement. This provides a seamless transition when content loads.

apps/nextjs/src/app/dashboard/friends/_components/friend-card.tsx (4)

8-9: Good type imports and UI component updates.

The changes appropriately import the necessary types from the API and UI components required for the enhanced friend card.

Also applies to: 19-21


33-35: Improved prop structure with type safety.

Refactoring the props to use the API response type provides better type safety and makes the component more maintainable as the friend data structure evolves.


54-56: Enhanced query invalidation for better data consistency.

Good improvement to invalidate both the friends list and the specific friend query, ensuring UI consistency across the application when a friend is removed.


132-133: Improved dialog text with dynamic friend name.

Using the friend's name in the dialog text makes the confirmation more personalized and clear to the user.

apps/nextjs/src/app/dashboard/friends/_components/friend-requests-tabs.tsx (4)

1-19: Properly structured client component with necessary imports.

The component correctly uses client-side rendering and imports all the necessary dependencies for state management, data fetching, and UI rendering.


20-28: Well-implemented state management and data fetching.

The component uses React state for managing the active tab and correctly implements suspense queries for data fetching. This allows for a clean component structure without explicit loading states.


30-55: Dynamic badge counts for improved user experience.

The tabs implementation with dynamic badge counts for both received and sent requests provides useful information at a glance. This is a great UX enhancement.


93-106: Consistent UI patterns between tabs.

Good job maintaining consistency between the "Received" and "Sent" tabs UI patterns, including the scrollable container, space styling, and empty state messages.

apps/nextjs/src/app/dashboard/friends/page.tsx (3)

4-7: Properly modularized component imports.

The updated imports reflect the more modular component structure, with separate components for the friends list, friend requests, and their respective skeleton loaders.


16-24: Improved responsive header layout.

The updated header layout with flex-col to flex-row transition at the md breakpoint provides better mobile support.


26-38: Enhanced responsive layout with individual loading states.

The updated grid layout with separate Suspense boundaries for each component is excellent. It provides a better user experience by:

  1. Allowing partial content to load independently
  2. Using specific skeleton components for each section
  3. Creating a better layout on both small and large screens with the grid-cols-1 to grid-cols-3 transition

This is a significant improvement over the previous implementation.

packages/api/src/routers/friends.ts (2)

119-123: Batch fetching Clerk users for better performance

You're correctly using getUserList to batch fetch Clerk users, which is more efficient than making individual requests for each user.


772-809: Consider using a Map to avoid duplicate game processing

Your implementation for building gameMap is efficient, using a Map to track unique games and aggregate statistics. This is a good pattern for avoiding duplicates and maintaining efficient lookups.

…ality

- Introduce a new FriendProfileSkeleton component for improved loading states on friend profile pages.
- Refactor FriendProfilePage to utilize the new skeleton component, enhancing user experience during data fetching.
- Implement FriendSettingsDialog and FriendSettingsForm components to manage friend-specific settings, improving user interaction and customization.
- Create FriendSharedItemsPage and related components to display shared items between friends, enhancing the overall functionality of the friends dashboard.
- Remove deprecated SharedGameCard and SharedPlayerCard components, streamlining the codebase and focusing on new shared items implementation.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx (1)

28-31: Add null checks for Clerk user properties

The code directly accesses Clerk user properties without null checks, which could lead to runtime errors if these properties are undefined.

          <Avatar className="h-16 w-16">
            <AvatarImage
-              src={friend.clerkUser.imageUrl}
-              alt={friend.clerkUser.name}
+              src={friend.clerkUser?.imageUrl}
+              alt={friend.clerkUser?.name || ""}
            />
            <AvatarFallback>
packages/api/src/routers/friends.ts (3)

119-119: ⚠️ Potential issue

clerkClient is not a function – fix all call sites.

The clerkClient is already initialized and doesn't need to be called with parentheses.

-const client = await clerkClient();
+const client = clerkClient;

Apply this fix to all instances at lines 119, 179, 238, 712, and 868.

Also applies to: 179-179, 238-238, 712-712, 868-868


141-155: 🛠️ Refactor suggestion

Extract getFullName into a reusable helper function.

The getFullName function is defined multiple times throughout this file with identical logic.

Create a utility function at the top of the file:

+const getFullNameFromClerkUser = (clerkUser: any) => {
+  if (clerkUser.fullName) {
+    return clerkUser.fullName;
+  }
+  if (clerkUser.firstName && clerkUser.lastName) {
+    return `${clerkUser.firstName} ${clerkUser.lastName}`;
+  }
+  if (clerkUser.firstName) {
+    return clerkUser.firstName;
+  }
+  if (clerkUser.lastName) {
+    return clerkUser.lastName;
+  }
+  return "Unknown";
+};

Then replace each local implementation with a call to this utility function.

Also applies to: 201-215, 258-272, 740-754, 879-893


124-132: ⚠️ Potential issue

Type mismatch: createdAt is included in the returned objects but missing in the inline type declarations.

The mapped objects include createdAt (returned at lines 164, 224, 280) but some type declarations omit it.

Add createdAt to all type declarations:

type MappedRequest = {
  id: number;
  status: "pending" | "accepted" | "rejected";
  name: string;
  userName: string | null;
  email: string | null;
  imageUrl: string | null;
+  createdAt: Date;
};

Consider defining these types once at the top of the file and reusing them to avoid inconsistencies.

Also applies to: 184-192, 242-249

🧹 Nitpick comments (12)
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-dialog.tsx (2)

57-58: Consider increasing the timeout value for better UX

The current 10ms timeout before closing the dialog may not be sufficient for the toast notification to be properly displayed or noticed by users. This might lead to a jarring experience where the dialog closes before users can see feedback.

-    setTimeout(() => setOpen(false), 10);
+    setTimeout(() => setOpen(false), 300);

69-69: Consider using max-height instead of fixed height for better responsiveness

The dialog has a fixed height of 750px which might cause issues on smaller screens. Using max-height with an appropriate overflow behavior would be more responsive.

-        <DialogContent className="h-[750px] sm:max-w-3xl">
+        <DialogContent className="max-h-[80vh] overflow-y-auto sm:max-w-3xl">
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx (2)

33-35: Extract avatar initials logic to a helper function

The complex logic for generating initials is embedded directly in the component, making it harder to read and test. Consider extracting this to a reusable helper function.

+          <AvatarFallback>
+            {getInitials(friend.clerkUser?.name || "")}
+          </AvatarFallback>
-          <AvatarFallback>
-            {(friend.clerkUser.name.split(" ")[0]?.substring(0, 1) ?? "") +
-              friend.clerkUser.name.split(" ")[1]?.substring(0, 1)}
-          </AvatarFallback>

You could add this helper function at the bottom of the file:

function getInitials(name: string): string {
  const parts = name.split(" ");
  const firstInitial = parts[0]?.substring(0, 1) || "";
  const lastInitial = parts[1]?.substring(0, 1) || "";
  return firstInitial + lastInitial;
}

153-172: Consider moving calculateHIndex to a utility file

The H-index calculation logic would be more reusable and testable if moved to a shared utility file, especially if it's used in multiple places.

This utility function contains domain-specific logic that could be reused in other components that display player statistics. Moving it to a shared location (like a utils folder) would improve code organization and testability.

apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-form.tsx (2)

94-134: Simplify form initialization with spread operator

The form initialization is verbose with all fields explicitly listed. You could simplify this using the spread operator, assuming the type safety is maintained.

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
-    defaultValues: initialSettings
-      ? {
-          autoShareMatches: initialSettings.autoShareMatches,
-          sharePlayersWithMatch: initialSettings.sharePlayersWithMatch,
-          includeLocationWithMatch: initialSettings.includeLocationWithMatch,
-          defaultPermissionForMatches:
-            initialSettings.defaultPermissionForMatches,
-          defaultPermissionForPlayers:
-            initialSettings.defaultPermissionForPlayers,
-          defaultPermissionForLocation:
-            initialSettings.defaultPermissionForLocation,
-          defaultPermissionForGame: initialSettings.defaultPermissionForGame,
-          autoAcceptMatches: initialSettings.autoAcceptMatches,
-          autoAcceptPlayers: initialSettings.autoAcceptPlayers,
-          autoAcceptLocation: initialSettings.autoAcceptLocation,
-          autoAcceptGame: initialSettings.autoAcceptGame,
-          allowSharedGames: initialSettings.allowSharedGames,
-          allowSharedPlayers: initialSettings.allowSharedPlayers,
-          allowSharedLocation: initialSettings.allowSharedLocation,
-          allowSharedMatches: initialSettings.allowSharedMatches,
-        }
+    defaultValues: initialSettings
+      ? { ...initialSettings }
      : {
          autoShareMatches: false,
          sharePlayersWithMatch: false,
          // ... rest of default values

177-325: Extract repeated form field patterns to a reusable component

There's significant duplication in the form field structure across all tabs. Consider creating a reusable component for toggle settings and permission selects to reduce code duplication.

You could create a component like:

function ToggleSetting({
  name,
  label,
  description,
  control,
  disabled = false,
}: {
  name: Path<z.infer<typeof formSchema>>;
  label: string;
  description: string;
  control: Control<z.infer<typeof formSchema>>;
  disabled?: boolean;
}) {
  return (
    <FormField
      control={control}
      name={name}
      render={({ field }) => (
        <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
          <div className="space-y-0.5">
            <FormLabel className="text-sm">{label}</FormLabel>
            <FormDescription className="text-xs">{description}</FormDescription>
          </div>
          <FormControl>
            <Switch
              checked={field.value}
              onCheckedChange={field.onChange}
              disabled={disabled}
            />
          </FormControl>
        </FormItem>
      )}
    />
  );
}

This would significantly reduce repetition in your form.

apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items.tsx (3)

51-64: Extract duplicated search input to a reusable component

The search input component is duplicated in both tab contents. Consider extracting it to a reusable component to reduce duplication and improve maintainability.

function SearchInput({ 
  value, 
  onChange, 
  placeholder = "Search shared items..." 
}: { 
  value: string; 
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; 
  placeholder?: string;
}) {
  return (
    <div className="relative mb-4">
      <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
      <Input
        placeholder={placeholder}
        className="pl-8"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}

Then use it like:

<SearchInput 
  value={withSearchTerm} 
  onChange={(e) => setWithSearchTerm(e.target.value)} 
/>

Also applies to: 66-79


94-119: Optimize item filtering for better performance

The filtering logic for items is complex and might be inefficient for large datasets. Consider optimizing this by:

  1. Memoizing the lowercase search term
  2. Using early returns for simple cases
  3. Using a simpler loop structure
  const filteredItems = useMemo(() => {
    if (!searchTerm) return items;

    const lowerSearchTerm = searchTerm.toLowerCase();
+   
+   // Quick check for empty search term
+   if (lowerSearchTerm === '') return items;
    
    return items.filter((item) => {
      // Check if item name matches search term
      if (item.name.toLowerCase().includes(lowerSearchTerm)) return true;

      // For game items, also check matches and scoresheets
      if (item.type === "game") {
-       // Check if any match name matches search term
-       const matchesMatch = item.matches.some((match) =>
-         match.name.toLowerCase().includes(lowerSearchTerm),
-       );
-       if (matchesMatch) return true;
-
-       // Check if any scoresheet name matches search term
-       const matchesScoresheet = item.scoresheets.some((sheet) =>
-         sheet.name.toLowerCase().includes(lowerSearchTerm),
-       );
-       if (matchesScoresheet) return true;
+       // Check match names and scoresheet names in a single pass
+       for (const match of item.matches) {
+         if (match.name.toLowerCase().includes(lowerSearchTerm)) return true;
+       }
+       
+       for (const sheet of item.scoresheets) {
+         if (sheet.name.toLowerCase().includes(lowerSearchTerm)) return true;
+       }
      }

      return false;
    });
  }, [items, searchTerm]);

This approach might be more efficient for larger datasets by avoiding multiple iterations through the arrays.


180-188: Improve type safety with generic TypeScript utility

The type extraction is verbose and could be simplified with TypeScript utilities.

function GameItem({
  item,
  searchTerm,
}: {
-  item: Extract<
-    RouterOutputs["friend"]["getFriend"]["sharedWith"][number],
-    { type: "game" }
-  >;
+  item: RouterItemType<"game">;
  searchTerm: string;
}) {

You could add this utility type at the top of the file:

type RouterItemType<T extends string> = Extract<
  RouterOutputs["friend"]["getFriend"]["sharedWith"][number],
  { type: T }
>;
packages/api/src/routers/friends.ts (3)

737-737: Remove debug console.log statement.

There's a console.log statement that should be removed before deploying to production.

-console.log(returnedFriend);

12-21: Type definition for game aggregation.

The GameAgg type provides clear structure for game statistics, but consider using a more specific discriminated union for the type field.

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type GameAgg = {
-  type: "Original" | "Shared";
+  type: "Original" | "Shared";  // Consider making this a discriminated union if properties differ by type
  id: number;
  name: string;
  imageUrl: string | null;
  plays: number;
  wins: number;
  winRate: number;
};

778-813: Game statistics calculation logic.

The code effectively calculates game statistics by aggregating match data, but consider extracting this logic to a separate utility function for better reusability.

This complex aggregation logic could be reused elsewhere in the application. Consider moving it to a utility function such as aggregateGameStats(matches, playerMatchData) in a separate file.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between b318e70 and 149b8bc.

📒 Files selected for processing (12)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-skeleton.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-dialog.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-form.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/page.tsx (3 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items-page.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/shared/page.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/stats/page.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/games/[id]/stats/_components/game-stats.tsx (1 hunks)
  • packages/api/src/routers/friends.ts (10 hunks)
✅ Files skipped from review due to trivial changes (2)
  • apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-skeleton.tsx
  • apps/nextjs/src/app/dashboard/friends/[id]/shared/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/nextjs/src/app/dashboard/friends/[id]/page.tsx
🧰 Additional context used
🧬 Code Graph Analysis (6)
apps/nextjs/src/app/dashboard/games/[id]/stats/_components/game-stats.tsx (1)
packages/shared/src/utils.ts (1)
  • getOrdinalSuffix (21-35)
apps/nextjs/src/app/dashboard/friends/[id]/stats/page.tsx (2)
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-skeleton.tsx (1)
  • FriendProfileSkeleton (4-40)
apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx (1)
  • FriendStatsPage (19-90)
apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx (2)
packages/ui/src/avatar.tsx (3)
  • Avatar (50-50)
  • AvatarImage (50-50)
  • AvatarFallback (50-50)
packages/shared/src/utils.ts (1)
  • formatDuration (14-19)
apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items.tsx (6)
apps/expo/src/utils/api.tsx (1)
  • RouterOutputs (57-57)
packages/ui/src/tabs.tsx (4)
  • Tabs (55-55)
  • TabsList (55-55)
  • TabsTrigger (55-55)
  • TabsContent (55-55)
apps/expo/src/lib/icons/Dices.tsx (1)
  • Dices (6-6)
packages/ui/src/badge.tsx (1)
  • Badge (37-37)
apps/expo/src/lib/icons/ChevronDown.tsx (1)
  • ChevronDown (6-6)
apps/expo/src/lib/icons/ChevronRight.tsx (1)
  • ChevronRight (6-6)
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-profile-page.tsx (2)
apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-dialog.tsx (1)
  • FriendSettingsDialog (29-110)
packages/shared/src/utils.ts (1)
  • formatDuration (14-19)
packages/api/src/routers/friends.ts (3)
packages/api/src/trpc.ts (1)
  • protectedUserProcedure (182-182)
packages/api/src/utils/sharing.ts (1)
  • collectShares (164-293)
packages/api/src/utils/game.ts (1)
  • mapMatches (3-262)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint
🔇 Additional comments (9)
apps/nextjs/src/app/dashboard/games/[id]/stats/_components/game-stats.tsx (1)

320-327: Removed optional chaining in rendering conditional

The optional chaining (?.) has been removed from subsequent accesses to teamPlayers[0].placement after the initial check. This is appropriate since the first condition already verifies that placement exists on teamPlayers[0], making the optional chaining redundant in the following expressions.

apps/nextjs/src/app/dashboard/friends/[id]/stats/page.tsx (1)

1-25: LGTM: Well-structured server component with proper error handling

This new server component follows best practices:

  • Properly validates the ID parameter and redirects on invalid input
  • Uses Suspense for asynchronous data loading with an appropriate fallback
  • Maintains a clean separation between server and client components

The component structure aligns well with the Next.js App Router pattern.

apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items-page.tsx (1)

1-45: LGTM: Well-implemented shared items component

This client component correctly:

  • Fetches friend data using suspense query
  • Displays the friend's profile with avatar and fallback
  • Passes the appropriate shared items data to the child component

The component follows the project's UI patterns and properly handles data flow.

apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx (1)

1-90: LGTM: Comprehensive friend stats component with good error handling

This component effectively:

  • Handles the case when a friend has no linked player by redirecting
  • Displays well-structured statistics with appropriate visual hierarchy
  • Uses proper formatting for duration with the shared utility function
  • Implements a responsive grid layout for different screen sizes

The component follows the project's UI patterns and integrates well with the friend management feature set.

apps/nextjs/src/app/dashboard/friends/[id]/_components/friend-settings-form.tsx (1)

117-133: Inconsistent default values may lead to confusion

The default values have autoAcceptGame set to true while other auto-accept fields are false. Similarly, all "allow" fields default to true. Ensure this difference is intentional and documented for clarity.

Review whether this difference in defaults is by design or if it should be consistent.

packages/api/src/routers/friends.ts (4)

121-123: N + 1 query optimization - batch-fetching Clerk users.

You've improved the code by using getUserList to batch-fetch users instead of individual API calls. This is a good performance optimization.

Also applies to: 181-183, 239-241


774-777: Good use of utility functions for match mapping.

Using the mapMatches utility function for consistent match mapping across the application improves code reusability and maintainability.


725-736: Efficient shared entity collection.

Using the collectShares utility to gather shared entities in both directions (from user to friend and friend to user) streamlines the code and ensures consistent data structure.


902-950: Complete implementation of friend settings management.

The new updateFriendSettings procedure provides comprehensive controls for friend-specific sharing preferences, supporting the PR's goal of introducing granular sharing controls.

Consider adding validation to ensure the friend actually belongs to the user before allowing settings updates. The current implementation checks this, but adding a comment explaining the security check would improve code clarity.

Gravender added 2 commits May 16, 2025 00:57
- Update FriendStatsPage to include HydrateClient for improved data handling.
- Refactor GameItem and SimpleItem components to apply consistent badge styling for shared items.
- Introduce GameDetails component to display detailed statistics of games played by friends, enhancing user experience.
- Improve match summary links to ensure proper identification of shared matches.
- Update API logic to handle game data more efficiently, including image handling and match type differentiation.
…est handling

- Update the friends router to utilize a new `getFullName` utility for improved clarity in retrieving user names from clerk user data.
- Refactor the FriendRequestCard component to handle loading states more accurately, enhancing user experience during friend request interactions.
- Remove redundant full name logic from the friends router, simplifying the codebase and improving maintainability.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
packages/api/src/utils/game.ts (1)

314-336: 🛠️ Refactor suggestion

Add type predicate to filter for improved type safety.

After mapping shared match players, the .filter((p) => p !== null) lacks a type predicate, which can cause TypeScript to still treat elements as potentially null.

-    .filter((p) => p !== null);
+    .filter((p): p is NonNullable<typeof p> => p !== null);
packages/api/src/routers/friends.ts (1)

120-120: ⚠️ Potential issue

Fix incorrect usage of clerkClient.

clerkClient is an object, not a function. Remove the function call operator to avoid runtime errors.

-const client = await clerkClient();
+const client = clerkClient;

This same issue occurs in both locations where clerkClient is used.

Also applies to: 668-668

🧹 Nitpick comments (6)
apps/nextjs/next.config.js (1)

19-20: Remove the duplicate picsum.photos entry.

You've added cdn.jsdelivr.net which is good for loading external images, but picsum.photos is already included in line 17, making the entry on line 20 a duplicate.

  { hostname: "cdn.jsdelivr.net" },
- { hostname: "picsum.photos" },
apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/game-details.tsx (2)

126-133: Consider keeping the "Shared" badge visible on mobile.

The "Shared" badge is currently hidden on mobile devices (hidden sm:block), which removes important context for users on smaller screens. Consider making this visible on all devices, possibly with a more compact design for mobile.

  <Badge
    variant="outline"
-   className="hidden bg-blue-600 text-xs text-white sm:block"
+   className="bg-blue-600 text-xs text-white"
  >
    Shared
  </Badge>

104-143: Add empty state handling for when no games are available.

The component doesn't handle the case when sortedGames is empty. Consider adding a message or placeholder when there are no games to display.

  <TableBody>
+   {sortedGames.length === 0 ? (
+     <TableRow>
+       <TableCell colSpan={4} className="h-24 text-center">
+         No games found.
+       </TableCell>
+     </TableRow>
+   ) : (
    {sortedGames.map((game) => {
      return (
        <TableRow key={`${game.id}-${game.type}`}>
          {/* existing content */}
        </TableRow>
      );
    })}
+   )}
  </TableBody>
packages/api/src/utils/game.ts (1)

272-273: Consider more detailed sorting criteria.

The current sorting uses simple date comparison. For matches occurring on the same date, consider adding secondary sorting criteria like match name or ID for more stable sorting.

-  allMatches.sort((a, b) => compareAsc(b.date, a.date));
+  allMatches.sort((a, b) => {
+    const dateComparison = compareAsc(b.date, a.date);
+    if (dateComparison !== 0) return dateComparison;
+    // Secondary sort by name for stable sorting of same-day matches
+    return a.name.localeCompare(b.name);
+  });
packages/api/src/routers/friends.ts (2)

693-693: Remove debug console.log statement.

There's a console.log statement that should be removed before production deployment.

-console.log(returnedFriend);

755-756: Consider sorting game statistics for consistent display.

The friendGames array is created from the gameMap values without any sorting, which may lead to inconsistent ordering in the UI.

-const friendGames = Array.from(gameMap.values());
+const friendGames = Array.from(gameMap.values()).sort((a, b) => {
+  // Sort by name as the primary key
+  return a.name.localeCompare(b.name);
+});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 149b8bc and e39e611.

📒 Files selected for processing (11)
  • apps/nextjs/next.config.js (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/game-details.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/[id]/stats/page.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx (2 hunks)
  • apps/nextjs/src/app/dashboard/games/[id]/[matchId]/summary/_components/match-summary.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/players/[id]/stats/_components/GameDetailsTable.tsx (1 hunks)
  • packages/api/src/routers/friends.ts (10 hunks)
  • packages/api/src/utils/clerk.ts (1 hunks)
  • packages/api/src/utils/game.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/api/src/utils/clerk.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/nextjs/src/app/dashboard/friends/[id]/stats/page.tsx
  • apps/nextjs/src/app/dashboard/friends/[id]/shared/_components/friend-shared-items.tsx
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx (7)
packages/ui/src/avatar.tsx (3)
  • Avatar (50-50)
  • AvatarImage (50-50)
  • AvatarFallback (50-50)
packages/shared/src/utils.ts (1)
  • formatDuration (14-19)
packages/ui/src/badge.tsx (1)
  • Badge (37-37)
apps/expo/src/lib/icons/Dices.tsx (1)
  • Dices (6-6)
packages/ui/src/calendar.tsx (1)
  • Calendar (68-68)
packages/ui/src/scroll-area.tsx (2)
  • ScrollArea (48-48)
  • ScrollBar (48-48)
apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/game-details.tsx (1)
  • GameDetails (25-147)
packages/api/src/routers/friends.ts (4)
packages/api/src/utils/clerk.ts (1)
  • getFullName (3-17)
packages/api/src/trpc.ts (1)
  • protectedUserProcedure (182-182)
packages/api/src/utils/sharing.ts (1)
  • collectShares (164-293)
packages/api/src/utils/game.ts (1)
  • mapMatches (3-274)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint
🔇 Additional comments (17)
apps/nextjs/src/app/dashboard/games/[id]/[matchId]/summary/_components/match-summary.tsx (1)

109-109: Good improvement to React key uniqueness.

Using a composite key that combines match.id and match.type is a better approach to ensure uniqueness when different types of matches might share the same ID. This follows React best practices for stable keys in lists.

apps/nextjs/src/app/dashboard/players/[id]/stats/_components/GameDetailsTable.tsx (1)

100-100: Good improvement to React key uniqueness.

Using a composite key that combines game.id and game.type ensures uniqueness when different types of games might share the same ID. This follows React best practices and maintains consistency with similar changes in other components.

apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/game-details.tsx (1)

1-148: Well-implemented component with good performance considerations.

The GameDetails component is well structured with proper sorting functionality, responsive design, and performance optimizations through memoization. The use of TypeScript for type safety is solid, and the component integrates well with the friend statistics feature described in the PR objectives.

apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx (5)

185-189: The cancel button loading state is now properly implemented.

The code correctly checks for the specific "cancel" loading state, addressing the issue noted in previous reviews.


80-102: Good implementation of the cancel friend request mutation.

The new mutation for canceling friend requests follows the same pattern as the other mutations, ensuring consistent error handling and query invalidation.


126-193: Well-structured UI with clear conditional rendering.

The component now elegantly handles both received and sent requests with appropriate UI for each case, using the type prop to conditionally render either Accept/Reject buttons or a Cancel button.


129-139: Nice avatar implementation with fallback.

The use of Avatar with AvatarFallback provides a good user experience when image URLs are unavailable, gracefully falling back to user initials.


143-146: Good date formatting with helpful visual cue.

The date display includes a clock icon and uses the formatDate utility, making it easy for users to see when the request was created.

apps/nextjs/src/app/dashboard/friends/[id]/stats/_components/friend-stat-page.tsx (4)

42-45: Good error handling with redirect.

Checking for the existence of the linked player and redirecting when absent prevents rendering errors and provides a better user experience.


147-205: Great implementation of conditional badge styling.

The badges for match status (Won/Lost/In Progress) use different styling to visually indicate the status, enhancing usability.


256-323: Excellent implementation of the horizontal scrollable match list.

Using ScrollArea with ScrollBar for the match list provides a clean and mobile-friendly way to browse through matches.


50-115: Well-structured summary card with comprehensive statistics.

The summary card presents key player statistics in a clean, organized layout that works well on both mobile and desktop views.

packages/api/src/utils/game.ts (1)

206-208: Good use of Set to prevent duplicate matches.

Using a Set with unique keys combining match ID and type is an effective way to prevent duplicate matches in the aggregated results.

packages/api/src/routers/friends.ts (4)

9-11: Good use of external utilities.

Importing and using shared utilities like getFullName, mapMatches, and collectShares improves code reusability and maintainability.


122-124: Efficiently fetching Clerk user data in batches.

Using getUserList with arrays of user IDs optimizes API calls to Clerk, resolving the previous N+1 query issue.

Also applies to: 167-169, 210-212


828-876: Well-implemented friend settings mutation.

The updateFriendSettings mutation properly handles both creating new settings and updating existing ones, with appropriate error handling.


714-778: Comprehensive statistics calculation for friend player data.

The code efficiently aggregates match data, calculates win rates, and identifies unique opponents, providing rich statistics for the UI.

… of requesteeId

- Modify the friends router to accept a username as input for sending friend requests, enhancing user experience by allowing searches by username.
- Implement error handling for cases where the user is not found in Clerk or the database, ensuring robust feedback for the user.
- Streamline the logic for inserting friend requests, improving clarity and maintainability of the codebase.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
packages/api/src/routers/friends.ts (6)

28-28: ⚠️ Potential issue

clerkClient is an object, not a function – remove the call operator

clerkClient is exported as an already-initialized SDK object. Calling it (await clerkClient()) throws TypeError: clerkClient is not a function at runtime.

-const client = await clerkClient();
+const client = clerkClient;

136-136: ⚠️ Potential issue

clerkClient is an object, not a function – remove the call operator

Similar to the issue in sendFriendRequest, using clerkClient as a function will cause runtime errors.

-const client = await clerkClient();
+const client = clerkClient;

181-181: ⚠️ Potential issue

clerkClient is an object, not a function – remove the call operator

Same clerkClient function call issue that needs to be fixed.

-const client = await clerkClient();
+const client = clerkClient;

224-224: ⚠️ Potential issue

clerkClient is an object, not a function – remove the call operator

Same clerkClient function call issue that needs to be fixed.

-const client = await clerkClient();
+const client = clerkClient;

684-684: ⚠️ Potential issue

clerkClient is an object, not a function – remove the call operator

Same clerkClient function call issue that needs to be fixed.

-const client = await clerkClient();
+const client = clerkClient;

825-825: ⚠️ Potential issue

clerkClient is an object, not a function – remove the call operator

Same clerkClient function call issue that needs to be fixed.

-const client = await clerkClient();
+const client = clerkClient;
🧹 Nitpick comments (2)
packages/api/src/routers/friends.ts (2)

709-709: Remove console.log statement before committing

Debug logging should be removed before production code is committed.

-console.log(returnedFriend);

166-166: Consider breaking down complex queries into smaller, more manageable functions

The query for getFriend has very deep nesting with multiple levels of relations. Consider breaking it into smaller queries or reusable query builders to improve readability and maintainability.

Also applies to: 448-677

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between e39e611 and 69dc4e2.

📒 Files selected for processing (1)
  • packages/api/src/routers/friends.ts (10 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/api/src/routers/friends.ts (4)
packages/api/src/trpc.ts (2)
  • createTRPCRouter (85-85)
  • protectedUserProcedure (182-182)
packages/api/src/utils/clerk.ts (1)
  • getFullName (3-17)
packages/api/src/utils/sharing.ts (1)
  • collectShares (164-293)
packages/api/src/utils/game.ts (1)
  • mapMatches (3-274)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: typecheck
  • GitHub Check: lint
🔇 Additional comments (6)
packages/api/src/routers/friends.ts (6)

161-161: Use the imported getFullName utility function from ../utils/clerk

You're correctly importing the getFullName utility function at line 9, which is good. This addresses a previous code duplication issue.


697-708: Good usage of the collectShares utility to aggregate share data

The implementation correctly aggregates shared entities for both directions of sharing (from user to friend and from friend to user), making this data easily accessible in the frontend.


731-794: Good implementation of match statistics aggregation

The code effectively:

  1. Maps matches from both original and shared sources
  2. Aggregates game statistics including wins, plays, and win rates
  3. Collects unique opponents
  4. Calculates overall stats like total wins and play duration

This provides comprehensive statistics for the friend profile UI.


868-891: Well-implemented upsert logic for friend settings

The mutation correctly handles both insert and update cases for friend settings, first checking if settings exist and then either creating or updating them accordingly.


138-140: Excellent use of batch fetching for Clerk users

Previous code likely suffered from N+1 query issues when fetching user data. This implementation correctly batches requests for Clerk users, which is more efficient and puts less strain on rate limits.

Also applies to: 183-185, 226-228


844-867: Well-structured schema for friend settings

The input validation schema for friend settings is comprehensive and type-safe. It covers all the necessary permissions and preferences for sharing different types of content between friends.

Gravender added 3 commits May 16, 2025 13:24
…orting

- Update the friends router to include a new `friendPlayer` field, improving the retrieval of linked player information for friends.
- Introduce a `linkedPlayerFound` boolean to indicate the presence of a linked player, enhancing data clarity.
- Sort friend games by name for better organization and user experience during game retrieval.
… date formatting

- Introduce a new FormattedDate component to standardize date display across various components, enhancing code reusability and maintainability.
- Replace direct date formatting logic with the FormattedDate component in multiple files, improving consistency in date presentation.
- Update imports and refactor existing components to utilize the new FormattedDate component, streamlining the codebase.
- Update the `mapSharedMatchPlayers` function to improve player mapping logic by utilizing a Map for unique playerId handling, enhancing performance and clarity.
- Streamline the return process by consolidating the mapping and filtering steps, resulting in cleaner and more maintainable code.
…uests

- Introduce the AddFriendDialog component to allow users to send friend requests via email or username, enhancing user interaction on the FriendsPage.
- Update the FriendsPage to include the AddFriendDialog, improving the overall functionality of the friends dashboard.
- Refactor the friends router to handle friend requests based on email or username, ensuring robust error handling and user feedback.
- Enhance the FriendRequestCard component to invalidate queries related to sent friend requests, improving data consistency.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
packages/api/src/routers/friends.ts (6)

184-185: ⚠️ Potential issue

Same error: clerkClient is an object, not a function

This is the same issue as above - clerkClient should not be called with parentheses.

-const client = await clerkClient();
+const client = clerkClient;

229-230: ⚠️ Potential issue

Same error: clerkClient is an object, not a function

This is the same issue as above - clerkClient should not be called with parentheses.

-const client = await clerkClient();
+const client = clerkClient;

273-274: ⚠️ Potential issue

Same error: clerkClient is an object, not a function

This is the same issue as above - clerkClient should not be called with parentheses.

-const client = await clerkClient();
+const client = clerkClient;

735-736: ⚠️ Potential issue

Same error: clerkClient is an object, not a function

This is the same issue as above - clerkClient should not be called with parentheses.

-const client = await clerkClient();
+const client = clerkClient;

877-878: ⚠️ Potential issue

Same error: clerkClient is an object, not a function

This is the same issue as above - clerkClient should not be called with parentheses.

-const client = await clerkClient();
+const client = clerkClient;

44-45: ⚠️ Potential issue

clerkClient is an object, not a function – remove the call operator

clerkClient is exported as an already-initialized SDK object from Clerk.
Calling it (await clerkClient()) will throw TypeError: clerkClient is not a function at runtime.

-const client = await clerkClient();
+const client = clerkClient;
🧹 Nitpick comments (1)
apps/nextjs/src/app/dashboard/friends/_components/add-friend-dialog.tsx (1)

128-142: Consider consolidating the conditional mutation call

The handleSubmit function has separate but nearly identical mutation calls for email and username types.

You could simplify this by using the spread operator:

function handleSubmit(value: string, type: "email" | "username") {
  setIsSubmitting(true);

-  if (type === "email") {
-    sendFriendRequestMutation.mutate({
-      type,
-      email: value,
-    });
-  } else {
-    sendFriendRequestMutation.mutate({
-      type,
-      username: value,
-    });
-  }
+  sendFriendRequestMutation.mutate({
+    type,
+    ...(type === "email" ? { email: value } : { username: value }),
+  });
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 842dc88 and 54e8763.

📒 Files selected for processing (4)
  • apps/nextjs/src/app/dashboard/friends/_components/add-friend-dialog.tsx (1 hunks)
  • apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx (4 hunks)
  • apps/nextjs/src/app/dashboard/friends/page.tsx (2 hunks)
  • packages/api/src/routers/friends.ts (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/nextjs/src/app/dashboard/friends/page.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx (3)
apps/expo/src/utils/api.tsx (1)
  • RouterOutputs (57-57)
packages/ui/src/hooks/use-toast.ts (2)
  • useToast (192-192)
  • toast (192-192)
packages/ui/src/avatar.tsx (3)
  • Avatar (50-50)
  • AvatarImage (50-50)
  • AvatarFallback (50-50)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: typecheck
  • GitHub Check: lint
🔇 Additional comments (12)
packages/api/src/routers/friends.ts (4)

189-197: The type used for mapping includes fields from implementation

The mapped type includes createdAt which is used in the implementation, but was missing in a previous version according to past review comments. This has been fixed by including it in the type.


747-759: Good implementation of shared content aggregation

The code effectively collects shared content in both directions (user to friend and friend to user) using the utility function collectShares. This creates a clean separation of concerns and good data organization.


761-776: Clean early return pattern for friends without linked players

The code properly handles the case where no linked player is found by returning early with the appropriate data structure. This is a good pattern that avoids unnecessary code nesting.


896-943: Well-structured friend settings mutation with proper validation

The updateFriendSettings mutation is well-implemented with:

  • Typed input validation using Zod
  • Proper database querying to check for existing settings
  • Conditional logic to either insert or update settings
  • Error handling for non-existent friends
apps/nextjs/src/app/dashboard/friends/_components/add-friend-dialog.tsx (3)

36-46: Clear schema validation for user inputs

The email and username schemas provide clear validation rules with descriptive error messages, which will help users understand input requirements.


51-68: Good dialog state management pattern

Explicitly managing the dialog open state with useState provides better control over the dialog lifecycle and makes the component more reusable.


152-261: Well-structured tabbed interface with coordinated form state

The implementation of the tabbed interface with separate form validation but unified submission handling is clean and maintainable. The UI provides clear user feedback during the submission process.

apps/nextjs/src/app/dashboard/friends/_components/friend-request-card.tsx (5)

16-19: Improved props interface with type safety

Using the RouterOutputs type for the friendRequest prop ensures type safety and consistent data structure throughout the application.


29-31: Expanded loading state to include cancel action

The loading state now properly includes "cancel" as a possible value, which improves the user experience by providing appropriate feedback during all async operations.


86-111: Well-implemented mutation for canceling friend requests

The cancelFriendRequestMutation follows the same pattern as the other mutations, ensuring consistent behavior and error handling.


135-203: Clean, responsive card layout with appropriate action buttons

The card layout with avatar, name, and formatted date provides a good user experience. The conditional rendering of buttons based on request type is well-implemented.


194-198: Correctly implemented loading state check for cancel button

The cancel button correctly checks for the specific "cancel" loading state, which was an issue mentioned in past reviews that has been fixed.

@Gravender Gravender merged commit 314f4ea into main May 16, 2025
4 of 6 checks passed
@Gravender Gravender deleted the feat/friends-settings branch May 16, 2025 13:58
Copy link

coderabbitai bot commented May 16, 2025

Caution

An unexpected error occurred while opening a pull request: Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant